Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created July 19, 2023 14:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mizchi/78aeed1947f87eded74b20ad8d9cb8b3 to your computer and use it in GitHub Desktop.
Save mizchi/78aeed1947f87eded74b20ad8d9cb8b3 to your computer and use it in GitHub Desktop.
marp theme
true
gaia

https://speakerdeck.com/mizchi/server-side-javascript-notamenobandoruzui-shi-hua のスライドのMarp のソースコード。 PDF だとリンククリックできなくて不便だったので


Server Side JavaScript のための

バンドル最適化

@mizchi / Workers Tech Talks #1 2023/07/19


自己紹介

Alt text

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

今日の前提知識

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

今日の前提破壊

Alt text


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


サーバーサイドでバンドルするメリット

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

JS 最適化の世界観 (フロントエンドの経験から)

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

Alt 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

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


CF-Workers の実行モデルのおさらい

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


CF-Workers のスクリプトサイズ制限

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

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

ビルドサイズによる 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
  • 考察
    • ビルドサイズによって、リリース直後やオートスケール時に低速化していそう

※ロングランではない雑なベンチです


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


Node.js のチューニング例


↑ を 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 に増えた

Node.js のチューニングから学べる教訓

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

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


mizchi/remix-d1-bullets


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/

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


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

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


パフォーマンスバジェット

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)


自分の結論

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

まとめ: Server Side JS のためのバンドル最適化

≒ フロントエンド最適化


余談: 罠踏みがちなライブラリの例

  • 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 効かないことが多い...


おわり

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment