Slide 1

Slide 1 text

Server Side JavaScript のための バンドル最適化 @mizchi / Workers Tech Talks #1 2023/07/19

Slide 2

Slide 2 text

自己紹介 @mizchi Frontend Ops / Frontend Performance Node.js + TypeScript 最近は TypeScript の型解析器を使う Minify を作ってる

Slide 3

Slide 3 text

今日の前提知識 bundle ESM/CommonJS で構成されるコードを単体ファイル+ 補助チャ ンクに結合する処理 webpack, rollup, esbuild --bundle (vite), swcpack 等 minify 意味を変えずに短いコードに変形する処理 terser, esbuild --minify, swc minify 等

Slide 4

Slide 4 text

今日の前提破壊

Slide 5

Slide 5 text

サーバーサイドのバンドル処理について考える

Slide 6

Slide 6 text

サーバーサイドでバンドルするメリット メリット 起動の高速化 : スピンアップ/ オートスケール高速化 CI: マルチステージビルドで node_modules を減らす セキュリティ : RSC / Remix Action のような Isomorophic 環境で 秘匿トークンが漏れ出ないようにする( 嬉しいというかマスト) デメリット SourceMap でツールチェイン複雑化 実行時相対パスに依存するせいでバンドル未対応のライブラリが ある(NestJS 等)

Slide 7

Slide 7 text

JS 最適化の世界観 ( フロントエンドの経験から ) 使っているライブラリのサイズ >>> 自分で書いたコード量 JS パフォーマンス ≒ ビルドサイズ スクリプト評価時間(CPU ブロッキング) もビルドサイズに比例

Slide 8

Slide 8 text

余談 : ESM vs CommonJS Deno Blog CommonJS is hurting JavaScript 要約: CommonJS はすべてが動的で、静的解析が難しい Bun Blog CommonJS is not going away 要約: ESM ではimport/export 両方に静的解析が必要なので初期 ロードが遅い (bun では @babel/core で 2.4x 遅い) => JS の bundle は一種の AOT Compile

Slide 9

Slide 9 text

CF-Workers におけるバンドル処理

Slide 10

Slide 10 text

CF-Workers の実行モデルのおさらい 要約:V8:Isolate で 128MB のメモリを割り当ててスクリプトを実行 https://developers.cloudflare.com/workers/learning/how-workers- works/

Slide 11

Slide 11 text

CF-Workers のスクリプトサイズ制限 wrangler が esbulid --bundle 相当の処理 意識しにくいだけで 必ず bundle されている --no-bundle はビルド済みの時にesbuild をスキップする用 スクリプトサイズの上限 Free Plan: 1MB Bundle Plan: 10MB ($5/m)

Slide 12

Slide 12 text

wrangler: bundle & minify gzip 後に 1MB を超えていると警告 $ pnpm wrangler deploy --dry-run --outdir dist --dry-run: exiting now. Total Upload: 8030.32 KiB / gzip: 1296.12 KiB ▲ [WARNING] We recommend keeping your script less than 1MiB (1024 KiB) after gzip. Exceeding past this can affect cold start time --minify $ pnpm wrangler deploy --dry-run --outdir dist --minify --dry-run: exiting now. Total Upload: 2932.68 KiB / gzip: 844.72 KiB

Slide 13

Slide 13 text

ビルドサイズによる CF-Workers 簡易ベンチ https://zenn.dev/mizchi/scraps/adc4938e203451 0.13kb と 1.2MB で同等のワーカーを作って比較( ほぼ dead code) 結果 0.13kb: だいたい 710~730req/s 1.2MB: デプロイ直後に 473req/s . 2 回目以降 670~690req/s 考察 ビルドサイズによって、リリース直後やオートスケール時に低速 化していそう ※ロングランではない雑なベンチです

Slide 14

Slide 14 text

Node.js と CF-Workers チューニングの方向性

Slide 15

Slide 15 text

Node.js のチューニング例 node.js のメトリクスの計測、ベンチマークの改善、Docker イメー ジの絞り方を勉強した - mizdev (3 年前) よくある Node.js+Express+React をチューニング ベースイメージを node から alpine+apk: 1.4GB => 108MB webpack でビルドして npm(-install) ごと消す: 108MB => 39MB

Slide 16

Slide 16 text

↑ を CF-Workers 視点で再チューニング https://github.com/mizchi/nodejs-benchmark-20230716 express を hono ( @hono/node-server ) で置き換えて 685K => 99K バンドル前後で HTTP listen するまでの初期化時間の比較 no-bundle( node lib/index.cjs ): 25ms bundled( node dist/index.js ): 2.4ms ついでに Docker イメージも修正してみたが... 最近のプラクティスに従って alpine から gcr.io/distroless/node にしたら 39MB => 160MB に増えた

Slide 17

Slide 17 text

Node.js のチューニングから学べる教訓 Docker イメージサイズ視点 イメージサイズの前では JS バンドルサイズは誤差 node_modules は制御しないとイメージサイズに響く ちゃんと (dev)dependencies 書き分けてますか? CF-Workers 視点 ランタイムは固定(v8) バンドルサイズこそがチューニング対象 ( フロントエンドと同じ) 共通: バンドルすることで初期化が( 今回の例では) 10 倍高速化

Slide 18

Slide 18 text

もう少し実践的な CF-Workers をみていく

Slide 19

Slide 19 text

mizchi/remix-d1-bullets https://github.com/mizchi/remix-d1-bullets @remix-run/cloudflare-pages: 49.9k remix-auth: 3.7k remix-validated-form: 46.6k zod: 57k dirzzle-orm: 24.2k remix: ? radix-ui: ? panda-css: ? (10k~)

Slide 20

Slide 20 text

mizchi/remix-d1-bullets のビルドサイズ $ pnpm install $ pnpm build:prod # worker のビルドサイズ $ la functions/ total 9416 621K [[path]].js 4.0M [[path]].js.map # node_modules 以下の合計 $ du -hs node_modules/ 690M node_modules/

Slide 21

Slide 21 text

svelte-kit のビルドサイズ $ npm create svelte@latest svelte-cf-worker # SvelteKit demo app を選択 $ npm i -D @sveltejs/adapter-cloudflare # ...svelte.config.js で adapter-cloudflare を使うように編集 $ npm run build ... $ la .svelte-kit/cloudflare/_worker.js 337K .svelte-kit/cloudflare/_worker.js https://kit.svelte.jp/docs/adapter-cloudflare

Slide 22

Slide 22 text

workers-rs のビルドサイズ Rust で動かす CF-Workers $ npx wrangler generate hello-world-rust \ https://github.com/cloudflare/workers-sdk/templates/experimental/worker-rust $ npx wrangler deploy --dry-run --minify $ la build/worker/ 343K index.wasm 12K shim.mjs これはほぼ最小の例で、例えば regex crate 入れると +700k https://zenn.dev/mizchi/scraps/413cd989324fc7

Slide 23

Slide 23 text

パフォーマンスバジェットを考えたい

Slide 24

Slide 24 text

パフォーマンスバジェット https://addyosmani.com/blog/performance-budgets/ A performance budget is a limit for pages which the team is not allowed to exceed. It could be a max JavaScript bundle size, total image weight, a specific load time (e.g Time-to-Interactive in under 5s on 3G/4G) or threshold on any number of other metrics. “ “ パフォーマンス予算とは、チームが超過することを許されないペ ージの制限のことです。JavaScript の最大バンドルサイズ、画像 の総重量、特定のロード時間(例:3G/4G でTime-to-Interactive が5 秒以下)、または他の指標のしきい値などです。 (Translated by DeepL) “ “

Slide 25

Slide 25 text

自分の結論 10MB は 普通の Node.js フルスタックサーバーを作る感覚だと超過 しうる CF−Workers 用のライブラリ選定は( 戦術の通り Docker で霞むの で) バンドルサイズを考慮してないことが多く罠が多い 例: @prisma/engine 35MB ( ほぼ Rust バイナリ) CDN Edge で動かすパフォーマンスメリットのためにも やっぱり 1MB をパフォーマンスバジェットとして設定したい

Slide 26

Slide 26 text

まとめ : Server Side JS のためのバンドル最適化 ≒ フロントエンド最適化

Slide 27

Slide 27 text

おまけ : 罠踏みがちなライブラリの例 core-js : 229.2kB @js-temporal/polyfill : 226.1kB @chakra-ui/react : 711kB element-plus : 1.3MB typescript : 2.8MB @prisma/engine : JS 1.5M + Binary 33M (Darwin) ※ https://bundlephobia.com の minify(not gzip) コンポーネントライブラリが treeshake 効かないことが多い...

Slide 28

Slide 28 text

おわり