ブログ一覧

Cloudflare Workers 無料プランの 3 MB 上限に数えられるもの

更新: Web開発者向け

Cloudflare Workers の無料プランには、Worker のバンドルサイズに gzip 後 3 MB という上限があります。ドキュメントに書いてある数字ではあるのですが、実際に運用してみると「何がこの 3 MB に数えられて、何が数えられないのか」がわかっていないと判断を誤ります。

私は 8 サイトを Workers 上で運用していて、そのうち 1 サイトに動的 OG 画像生成を入れようとして、この上限に達しました。画像生成ライブラリを 1 つ足しただけで、余裕は一気に消えました。 一方で、同じサイトの静的アセットは合計 11 MB あるのに何の問題も起きていません。

上限に数えられるのはバンドルに取り込まれるコードで、静的アセットは数えられません。実測値とあわせて整理します。

上限の数字

Cloudflare のドキュメントに記載されている、Worker あたりのバンドルサイズ上限は以下のとおりです。

  • Free プラン: gzip 後 3 MB
  • Paid プラン: gzip 後 10 MB

この上限は圧縮後の値です。ビルド出力のファイルサイズをそのまま見て「まだ余裕がある」と判断しても意味がありません。逆に、非圧縮では数 MB あっても gzip でよく縮む JavaScript なら通ります。

測り方

wrangler deploy に --dry-run を付けると、デプロイせずにサイズだけを出せます。

npx wrangler deploy --dry-run

出力の最後のほうに、こういう行があります。

Total Upload: 9066.38 KiB / gzip: 2193.94 KiB

見るべきは `gzip:` の後ろです。手前の Total Upload は非圧縮のサイズで、上限とは直接対応しません。この例では非圧縮 9 MB に対して圧縮後は 2.14 MB。JavaScript は 4 分の 1 近くまで縮むので、非圧縮の数字だけ見ていると必要以上に慌てることになります。

実測ベースライン

私が運用している Astro + EmDash 構成のサイトを 2 つ測ると、こうなりました。

  • サイト A(この shinobiworks): 非圧縮 9066 KiB → gzip 後 2194 KiB。3 MB まで残り約 880 KiB
  • サイト B(別サイト): 非圧縮 8654 KiB → gzip 後 2100 KiB。3 MB まで残り約 970 KiB

構成がほぼ同じなのでどのサイトも 2.1 MB 前後で、Free プランの 3 MB に対して残っている余裕は 900 KB 程度しかありません。

この「900 KB」という数字を知っているかどうかで、ライブラリを 1 つ足すときの判断がまるで変わります。

数えられないもの: 静的アセット

`public/` に置いた静的アセットは、バンドルサイズ上限の対象外です。Cloudflare のドキュメントも、設定ファイルやバイナリデータはバンドルに含めず KV・R2・D1・Workers Static Assets に置くよう明示的に推奨しています。

実際、サイト A のビルド出力を見るとこうなっています。

$ du -sh dist/client
11M	dist/client

静的アセットだけで 11 MB あります。 3 MB の上限をとうに超えていますが、デプロイは通ります。--dry-run の出力にも、これらは別枠として現れます。

✨ Read 59 files from the assets directory .../dist/client
Total Upload: 9066.38 KiB / gzip: 2193.94 KiB

「アセットディレクトリから 59 ファイル読んだ」という行と、Total Upload は別勘定です。画像やフォントをいくら足しても Worker のサイズは増えません。

静的アセットが数えられないことを知らないと、「画像を追加したらデプロイが落ちるかもしれない」と心配して画像を削りがちですが、画像を削ってもバンドルサイズは 1 バイトも減りません。

数えられるのは JavaScript と wasm

では何が 900 KB の余裕を減らすのか。バンドルに取り込まれる JavaScript と WebAssembly です。とくに wasm は gzip で縮みにくく、ほぼ元のサイズのまま数えられます。

冒頭に書いた動的 OG 画像生成では、JavaScript と wasm の両方をバンドルに取り込みました。記事タイトルを入れた OG 画像を実行時に生成するため、satori + resvg-wasm(workers-og)を 1 ルートぶんだけ追加しました。結果:

  • 追加前: gzip 後 2.29 MB
  • 追加後: gzip 後 2.97 MB
  • 差分: 約 +680 KB

1 ルート追加しただけで、3 MB の上限の手前まで増えました。 内訳の大半は resvg の wasm です。動作自体は問題なく、生成時間も初回 109ms・2 回目以降は Cache API ヒットで 4ms と実用範囲でしたが、残り 30 KB では今後どんな依存も足せません。 この機能は見送りました。

「ライブラリを 1 つ足す」という日常的な判断が、Free プランではデプロイ不能に直結します。実行時に画像生成・PDF 生成・重い変換を行う系のライブラリは、入れる前に測るのが鉄則です。

バイナリは静的アセットに置く

同じ OG 画像の実装で、フォントは意図的にバンドルから外しました。

日本語フォントは大きく、Noto Sans CJK JP のフルセットは 1 ウェイト 16 MB あるので、そのままではどこにも置けません。収録範囲を変えながら測るとこうなりました。

  • かな・記号・約物のみ: 生 0.10 MB / gzip 0.07 MB
  • cp932(JIS X 0208 + NEC/IBM 拡張、10,285 字): 生 1.65 MB / gzip 1.27 MB
  • CJK 統合漢字 全域(U+4E00-9FFF): 生 5.03 MB / gzip 3.94 MB
  • さらに拡張 A・互換漢字まで: 生 6.71 MB / gzip 5.32 MB

「いま公開している記事で実際に使われている文字だけ」に削る手もありますが、これは採れません。新しい記事で収録範囲外の漢字を 1 文字でも使った瞬間、その字が □ に化けて表示されます。 収録範囲は cp932 にしました。cp932 なら、日本語のブログタイトルとして書かれうる文字をほぼ網羅して 1.65 MB に収まります。

とはいえ、cp932 のフォントを 2 ウェイトぶんバンドルに入れると gzip で +2.5 MB になり、この追加だけで 3 MB の上限を超えます。 そこで public/fonts/ に置き、実行時に fetch してモジュールスコープに保持し、同じ isolate 内で使い回す形にしました。静的アセットに置けば、フォントが 3.3 MB あってもバンドルサイズはゼロです。

判断の順序としてはこうなります。

  1. その依存は実行時に本当に必要か(ビルド時に済ませられないか)
  2. 必要なら、バイナリ部分を静的アセットに移せないか
  3. 移せないなら、測ってから入れる

もう一つの上限: 起動 1 秒

サイズのほかにも制約があり、ドキュメントには、Worker はグローバルスコープ(ハンドラの外側のトップレベルコード)を 1 秒以内にパース・実行し終える必要があると書かれています。そして「バンドルが大きいほど、またグローバルスコープでの初期化が重いほど起動時間は伸びる」と明記されています。

つまり 3 MB に収まっていても、トップレベルで重い初期化をしていれば別の理由で落ちます。大きな wasm をトップレベルでインスタンス化する実装は、サイズと起動時間の両方を同時に押し上げるので、とくに危険です。

いつ測るか

サイズ超過は、依存を足した PR の時点では分かりません。

PR ごとに型チェックとテストは全サイトぶん自動で走りますが、これらはバンドルサイズを見ません。サイズが確定するのは実際に Workers へ上げる段です。私の構成では、Workers へ上げるところまで実行する Preview デプロイを用意しているのは 8 サイトのうち 1 サイトだけです。個人で運用している小規模なサイト群なので、全サイトぶんのプレビュー環境は意図的に持っていません。

実害の範囲は限定的です。サイズ超過で失敗するのはデプロイ処理そのものなので、新しいバンドルが上がらないだけで、現行の Worker はそのまま動き続けます。 それでも気付くのが main への merge 後になるのは遅いので、重い依存を足す PR では手元で測る、というのを習慣にしました。

pnpm --filter <app> build
npx wrangler deploy --dry-run    # gzip: の値を 3 MB と比べる

余裕が 900 KB しかないとわかっていればこの 2 コマンドを打つ気になるので、いまのサイズを知らないことが最大のリスクです。

まとめ

  • Free プランの Worker バンドル上限は gzip 後 3 MB(Paid は 10 MB)
  • 測るのは wrangler deploy --dry-run の `gzip:` の値。Total Upload(非圧縮)ではない
  • 静的アセットは上限の対象外。画像やフォントをいくら置いても Worker のサイズは増えない(実測で 11 MB のアセットを抱えたまま問題なく動いている)
  • サイズを増やすのは JavaScript と wasm。画像生成ライブラリ 1 つで +680 KB 増え、3 MB の上限の手前まで届く
  • バイナリは public/ に置いて実行時に fetch する。フォント 3.3 MB がバンドルサイズゼロになる
  • サイズとは別に起動 1 秒の制約がある。トップレベルの重い初期化はサイズと起動時間の両方を押し上げる
  • 型チェックやテストではサイズは見えない。重い依存を足す PR では手元で測る(失敗してもサイトは止まらないが、気付くのは遅くなる)

無料プランで何ができるかという話は、Cloudflare Pages の無料プランは商用利用できるのかやVercel・Netlify・Cloudflare Pages の無料プラン比較にも書いています。無料プラン特有の制約という意味では、Cloudflare Pages 無料プランで next/image が使えない話も同じ系統の話です。

この記事をシェア