Cloudflare Pages はもう Workers の中に住んでいる ― Static Assets 統合後の地図

未分類

無料でエッジに静的サイトを撒けるのが気持ちよくて Cloudflare Pages を愛用してきた。最近のタイムラインで「Workers の Static Assets を使ったほうがいい」と書かれているのを何度か見かけて、引用元のポストもまさにそれだった。

Cloudflare Workersのstatic assetsを使えば100万アクセス/月あっても0円なのでオススメです

→ 元ポスト(@catnose99, 2026-06-27)

自分も wrangler を叩いてデプロイしているし、なんとなく Pages を使っている気がしているが、ワーカーズの中にページズが入っているのかいないのか、結局どっちで動いているのか、毎回曖昧なまま済ませてきた。

ここで一度きちんと整理する。結論を先に出す。

  • Cloudflare Pages は廃止されない。ただし新機能は Workers にしか乗らなくなった
  • Workers に Static Assets が組み込まれ、「実質 Pages」と同じことが Workers の上でできるようになった
  • 静的アセットのリクエストは Pages も Workers も無料・無制限。違うのは Functions / Worker code を呼んだ時の課金枠の数え方
  • 自分の 2 プロジェクトのうち、mdx-playground は完全 SSG だから移行しなくても無料eurekapu-nuxt4 は SSR が混じるので Functions 課金枠が走るが、_routes.json の exclude を入れてあるので静的アセット分は Functions を経由していない。Workers に移しても同じ最適化が効く

Before / After の地図

Cloudflare Pages と Workers + Static Assets の構成比較

左が従来の Pages、右が Workers + Static Assets の構成。図の見どころは 3 つだけだ。

  1. どちらも「静的アセット」と「Worker(=SSR/API)」の組み合わせでできている。製品名が違うだけで、中身は同じ部品
  2. Pages は _routes.json で「Functions を通す URL / 通さない URL」を仕分ける。Workers は逆に既定で静的アセットが優先され、必要な URL だけ run_worker_first で Worker を先に呼ぶ
  3. 無料枠の境目は同じ。静的アセットへのリクエストはどちらも無制限・無料。課金枠を消費するのは Worker コードを呼んだ時だけ

つまり「Pages の上に Functions(= Worker)が乗っている」のと「Workers の中に Static Assets が同梱されている」のは、責任の所在を反転させただけで、構成図としてはほぼ同じになる。

Cloudflare の Workers チームリードである Kenton Varda が「Pages 固有の機能を一般的な Workers の機能に変換している」と発言していて(→ Cloudflare Pages vs Workers 2026 / cogley.jp)、これが現状を一番うまく要約している。Pages は殺されるのではなく、Workers の中に吸収されつつある

「廃止される」ではなく「飲み込まれる」

廃止と統合は意味が違うので、分けて書く。

  • 既存の Pages プロジェクトは当面そのまま動き続ける。マイグレーション期限はまだ発表されていない
  • ただし、新機能(Durable Objects、Cron Triggers、Queues、Workflows、Tail Workers、Smart Placement など)は最初から Workers にしか乗らない
  • そのため、いま新規プロジェクトを始めるなら最初から Workers + Static Assets を選ぶのが推奨経路になる

ここを誤解して「Pages はもう使えない」と書くと言い過ぎになる。逆に「Pages のままでもいい」と書くと、新機能が必要になった瞬間に詰む。いまは「動いてるものは動かしておけ、新しく作るなら Workers で」が落としどころになっている。

無料枠の現在地

「無料」の話が曖昧になりがちなので、自分が触る範囲だけ整理する。

  • 静的アセット:Pages、Workers どちらも リクエスト数も帯域も無制限・無料。これが catnose 氏のポストで「100 万アクセス / 月でも 0 円」と書かれている根拠
  • Worker code(= SSR / API):Free プランで 10 万 req / 日、Paid($5 / 月)で 1000 万 req / 月 が基本枠
  • 転送量:Cloudflare は egress を課金しない(これは Pages 時代から)

つまり「サーバー処理が要らない静的サイト」は何アクセスあろうが完全無料、というのが Cloudflare 流のオフロード設計だ。自分の mdx-playground は 1000 本超の記事を SSG で全部 HTML 化しているので、ここの無料枠にきれいに乗っている。

課金の単位は「ファイルサイズ」ではなく「isolate を起こした回数」

ここまで読んで「画像 1 枚を返すだけで Worker invocation 課金枠が走るって、Cloudflare の中で何がそんなに起きているのか」が腹落ちしないと、_routes.jsonexclude を入れる動機もぼんやりする。中で何が起きているかを切り分けて書く。

Cloudflare の各 PoP(世界 300 拠点ほど)にはリクエストを受けたあとの分岐が物理的に 2 本ある。

(A) 静的アセット配信レーン Cloudflare の内部分散ストレージから、ファイルのバイト列をそのまま返す。実装は Cloudflare ネイティブの C++/Rust の HTTP サーバで、リクエストごとに JS ランタイムを起こさない。ここを通る分には Worker invocation はゼロ

(B) Worker レーン V8 isolate(Chrome の JS エンジンを軽量隔離したコンテナ)をスピンアップして、自分の書いた export default { fetch(req, env) { ... } } を実行する。レスポンスは isolate が組み立てて返す。この isolate が一度起動したら invocation 1

Pages では、リクエストがエッジに着くとまず _routes.jsoninclude / exclude を見て、「この URL は (A) か (B) か」を決める。

{
  "version": 1,
  "include": ["/*"],         // 既定: 全部 (B) Worker レーン
  "exclude": ["/_nuxt/*", "/images/*"]   // ここだけ (A) 静的レーン
}

eurekapu-nuxt4 の nuxt.config.ts_routes.json の exclude に /images/*/_nuxt/* を入れているのは、まさにこの分岐を Edge に教えるためのもので、入れ忘れると画像も CSS も JS も全部 Worker レーン経由になる。

画像 1 枚で課金される理由

Worker レーンに入ったときに、Nuxt SSR が画像を返す経路で実行されるコードは、結局のところ次の 1 行に近い。

export default {
  async fetch(request, env) {
    return env.ASSETS.fetch(request);  // 内部の静的ストレージを叩いて画像を返すだけ
  }
}

ASSETS.fetch(...) は内部で (A) の静的ストレージを叩きに行くので、転送量は egress 無料、画像のバイト数も無料。それでもエッジは

  1. V8 isolate を起こす
  2. fetch handler を呼ぶ
  3. レスポンスをストリームに書き戻す
  4. isolate を解放する

を必ず通る。Cloudflare の課金体系は、この一連を「Worker invocation 1」とカウントする。中身が画像か API か SSR か、CPU をどれだけ食ったかは別の枠(CPU time)で計測されるが、invocation カウントは「isolate を呼んだか / 呼ばなかったか」だけで決まる

つまり、画像本体のバイト数ではなく、画像を返すために isolate を 1 回起こした事実そのものが課金単位になる。1000 アクセスで画像 30 枚を Worker レーン経由で返したら、invocation が 30,000 回分溶ける。exclude を入れて画像 URL を (A) に逃がすと、Edge は「この URL は isolate を起こさないでいい」と判断するので 0 のまま。

Workers + Static Assets で既定が逆になっている理由

このカラクリがわかると、Workers + Static Assets で既定の振り分けが逆になっている理由も自然に読める。

  • Pages: include: ['/*'] が既定 → 既定が (B) Worker レーン → 静的にしたい URL を exclude で外す
  • Workers: 静的アセット配信が既定 → 既定が (A) 静的レーン → Worker を走らせたい URL を run_worker_first指名する

Cloudflare 自身が「Pages の既定は安全側じゃなかった(うっかり全 URL を Worker 経由にすると課金枠を無駄に食う)」と認めて、Workers + Static Assets ではデフォルトを反転させた。これが Pages → Workers 統合の背景にある実務的な動機の 1 つになっている。

自分の 2 プロジェクトはどうなっているか

自分の2プロジェクトの現状

mdx-playground(log.eurekapu.com)

apps/web/nuxt.config.ts の Nitro 設定はこうなっている。

nitro: {
  preset: "cloudflare-pages-static",
  prerender: {
    crawlLinks: true,
    routes: ['/blog', '/', '/beat-monitoring', '/memory-makers'],
    ignore: [/\?/, '/search', ...],
    failOnError: false,
  },
},

cloudflare-pages-staticPages のうち「静的アセットだけ」を使うプリセット。Functions は一切作られず、dist/ には HTML / CSS / 画像しか入らない。動的処理は _redirects の単純な URL 書き換えだけで、それも Cloudflare の Pages 側で評価される(コンテナを起こさない)。

ということは、Worker code が呼ばれる回数は実質ゼロで、無料枠も消費していない。Workers + Static Assets に移しても挙動も料金も変わらないから、急いで移行する理由はない。新しいブログを作るときに最初から Workers preset を選べばいい、くらいの優先度になる。

eurekapu-nuxt4(info-accounting.com)

こっちは認証あり・Stripe あり・D1 ありの本格的なアプリで、nuxt.config.ts がこうなっている。

nitro: {
  preset: 'cloudflare_pages',
  sourceMap: false,
  cloudflare: {
    pages: {
      routes: {
        include: ['/*'],
        exclude: [
          '/_nuxt/*',
          '/images/*',
          '/favicon.ico',
          '/robots.txt',
          ...redirectExcludes,
        ],
      },
    },
  },
},

cloudflare_pages は SSR 込みの Pages プリセット。_routes.jsonexclude で「_nuxt/*images/* は Pages Functions を通さない(=静的アセットとして直接配信する)」と明示している。

この exclude を入れた理由がまさに今回の話と地続きで、何も指定しないと「全 URL が Pages Functions(= Worker)経由」になってしまい、画像 1 枚配るだけで Worker invocation 課金枠を消費する。_routes.json の exclude で「静的でいいものは静的に流す」と切り分けることで、無料枠を温存できる。

これを Workers + Static Assets に移すと、考え方が逆になる

  • Pages:「全部 Functions、ただし exclude で静的に逃がす」
  • Workers:「全部静的、ただし run_worker_first で SSR を挟む」

責任のデフォルトが反転するだけで、最適化の発想は同じだ。だからこの構成も、いますぐ移す必要はない。ただし Durable Objects や Cron が欲しくなった瞬間に Workers 側でしか書けないので、機能要件が伸びれば移行が前提になる。

「Workers の中にページ図が入ってる」の腹落ち

最初に持っていた素朴な疑問「ワーカーズの中にページ図が入っているのか?」は、半分正解で半分逆だった。

  • 正解の半分:いまの Workers には Pages 相当の Static Assets 配信が入っている。Pages でできていたことは Workers でほぼ全部できる
  • 逆の半分:歴史的には Pages のほうが先で、Pages の中に Functions(= Worker)が住んでいた。それを Workers 側に移して、「Workers の中に Static Assets が住む」構造に組み直しているのが現在進行形のリファクタリング

つまり「Pages も Workers も静的+関数のセット」という構成自体は一貫していて、どっちのブランドの皮を被るかだけが変わっている。新しい皮(Workers)に未来の機能が乗ってくるから、新規はそちら、というのが Cloudflare の言いたいこと。

自分の運用への落とし込み

ここまで踏まえて、自分はこう動くつもりでいる。

  • mdx-playground:このまま cloudflare-pages-static を続ける。完全 SSG なので Workers に移しても得るものがない。むしろビルドパイプラインを壊すリスクのほうが大きい
  • eurekapu-nuxt4:当面 Pages のままだが、Cron や Durable Objects を使った機能(決算ビートの定期取得など)を新規に作るときは、そのモジュールだけ別 Worker として切り出す。アプリ本体まで含めた全面移行は急がない
  • これから作る新規プロジェクト:最初から Workers + Static Assets で組む。preset は Nuxt なら cloudflare-module 系、フレームワークなしなら wrangler init の Static Assets テンプレ

「Pages 推奨だったはずなのに気付いたら Workers 推奨になってた」と感じたのは、Cloudflare 自身がブランドを差し替えに行っている最中だからで、自分の感覚は間違っていなかった。ただ、慌てて全部移す必要もない。動いているものはそのまま、新しいものは Workers で、で十分間に合う。

参考