nun_game0

Cloudflare WorkersにNext.jsをデプロイして詰まったポイントまとめ

Cloudflare Workersデプロイ

このサイトはNext.jsで作っているけど、ホスティング先はVercelではない。Cloudflare Workersで動かしている。その経緯とハマりポイントを書く。

なぜCloudflare Workers

Vercelでも良かった。むしろNext.jsとの相性ならVercelが最適解で、公式が作っているフレームワークを公式のプラットフォームで動かすのが一番トラブルが少ない。それはわかっている。

でもインフラエンジニアとしては、Cloudflareのエッジネットワークで動かしたかった。仕事でCloudflareを扱う場面が増えてきて、Workers、R2、D1あたりの勘所を個人サイトで掴んでおきたいという動機が大きい。Vercelにデプロイするだけだと、インフラ側の学びがほとんどない。

あと、無料枠が太い。Workers Free Planは1日10万リクエストまで無料。個人ブログのトラフィックなら余裕で収まる想定だった(実際にはCPU時間の制限で有料プランに切り替えることになるのだけど、それはコストの記事に書いた)。

OpenNextという選択肢

Next.jsをVercel以外で動かす手段はいくつかあるけど、Cloudflare Workersに載せるなら @opennextjs/cloudflare が現状の定番。Next.jsのビルド出力をWorkers互換のフォーマットに変換してくれるアダプターで、コミュニティ主導のOSSプロジェクト。

npm install -D @opennextjs/cloudflare

ビルドは2段階になる。まず next build で通常のNext.jsビルドを実行して、その出力を opennextjs-cloudflare build でWorkers向けに変換する。package.jsonscripts にまとめておくと楽。

{
  "scripts": {
    "build": "next build",
    "build:worker": "opennextjs-cloudflare build",
    "deploy": "opennextjs-cloudflare deploy"
  }
}

wrangler.jsonc の設定も必要。自分の場合は main にOpenNextが生成するWorkerのエントリポイントを指定して、compatibility_flagsnodejs_compat を追加した。これがないとNode.js由来のAPIが一切使えなくてビルドが通らない。

ビルドフロー

ハマったポイント

next/ogが動かない

一番ハマったのがOG画像の動的生成。next/ogImageResponse はCloudflare Workersのエッジランタイムで動かない。内部でWebAssembly(Satori + Resvg)を使っていて、Workersのメモリ制限(128MB)に引っかかる。

ローカルの wrangler dev では動くのに、本番デプロイすると500エラーになるというタチの悪いパターン。ログを見ると RangeError: WebAssembly.instantiate(): Out of memory みたいなエラーが出ていた。

いくつか対策を試した。フォントファイルを小さいサブセットに変更する、画像のサイズを小さくする、Satoriのオプションを最小構成にする。どれも効果なし。最終的にはWorkers上での動的OG生成は諦めて、ビルド時に静的なOG画像を生成する方式に切り替えた。完璧じゃないけど、壊れたOG画像が表示されるよりはマシ。

Node.js APIの制限

Cloudflare Workersは完全なNode.jsランタイムではない。nodejs_compat フラグを有効にしても、fs モジュールのようなファイルシステム系のAPIは使えない。pathcrypto は使えるけど、すべてのNode.js APIが揃っているわけではない。

これが厄介なのは、自分のコードで直接 fs を使っていなくても、依存パッケージの内部で使っていると動かないこと。ビルドは通るのに実行時にエラーになるケースがあって、原因の特定に時間がかかった。

対策として、ファイルシステムへのアクセスはすべてビルドスクリプト内に限定した。ブログのMarkdownファイルは scripts/build-blog.ts でJSONに変換しておいて、ランタイムではそのJSONを import で読むだけにしている。こうすればWorkersのランタイムで fs に触る必要がない。

// ビルド時: Markdownを読んでJSONに変換
// scripts/build-blog.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

const posts = fs.readdirSync(contentDir).map(file => {
  const raw = fs.readFileSync(path.join(contentDir, file), 'utf-8');
  const { data, content } = matter(raw);
  return { slug: data.slug, title: data.title, content };
});

fs.writeFileSync('public/blog-data.json', JSON.stringify(posts));
// ランタイム: JSONをimportして使う
import blogData from '@/public/blog-data.json';

バンドルサイズの壁

Workersにはスクリプトサイズの上限がある。Free Planだと1MB、Paidプランでも10MB。Next.jsのビルド出力は依存パッケージを含めるとかなりのサイズになるから、この上限に引っかかることがある。

自分の場合、gray-mattermarkedshiki(シンタックスハイライト)を全部ランタイムバンドルに含めようとして上限を超えた。対策は単純で、これらを devDependencies に入れてビルド時だけ使う設計に変えた。ランタイムではビルド済みのHTMLを返すだけだから、これらのパッケージは不要。

next.config.tsserverExternalPackages で除外する方法もあるけど、Workers環境では除外したパッケージが存在しないのでエラーになる。ビルド時に完結させる方が確実。

wrangler.jsoncの罠

地味にハマったのが wrangler.jsonc の設定。compatibility_date を古い日付にしていると、一部のAPIが使えなかったり挙動が変わったりする。自分は最初 2024-01-01 にしていて、Response.json() の挙動が微妙に違うことに気づくのに30分くらいかかった。

基本的には compatibility_date は可能な限り新しい日付にしておくのが無難。

ハマりポイント

デプロイの流れ

実際のデプロイフローはシンプル。

  1. npm run build でNext.jsビルド + ブログJSON生成
  2. npx opennextjs-cloudflare build でWorkers向けに変換
  3. npx opennextjs-cloudflare deploy でデプロイ

デプロイ自体は1〜2分で終わる。Cloudflareのダッシュボードでデプロイ履歴も確認できるし、ロールバックもワンクリック。ここはVercelと同じくらい快適。

GitHubにプッシュしたら自動デプロイする構成(GitHub Actions + wrangler deploy)も組めるけど、今のところ手動で運用している。記事の更新頻度が週1〜2回程度なので、ターミナルで3コマンド叩く方が手っ取り早い。記事数が増えてきたら自動化を検討する。

パフォーマンス

Cloudflare Workersのエッジで動くので、レスポンスは速い。東京リージョンからのアクセスで、TTFBは50ms以下。CDNキャッシュが効いている場合は10ms以下になることもある。

キャッシュ戦略は Cache-Control ヘッダーで制御している。静的ページは s-maxage=86400(1日)でCDNキャッシュ、stale-while-revalidate=3600 でバックグラウンド更新。動的なAPIレスポンスはキャッシュしない。

Lighthouseのスコアはパフォーマンス98〜100。SSGで生成した静的ページがメインなので、これは当然といえば当然。Next.jsのどこにデプロイしても同じスコアが出ると思う。

Vercelとの比較

正直に言うと、個人サイトならVercelの方が楽。圧倒的に楽。GitHubリポジトリを接続して、フレームワークのプリセットを選ぶだけ。wrangler.jsonc の設定も、OpenNextの互換性問題も、バンドルサイズの最適化も全部不要。

Cloudflare Workersを選ぶメリットは、エッジコンピューティングの実践的な学び、Cloudflareエコシステム(DNS、WAF、R2、D1)との統合、そして月$5〜$6で全部まかなえるコスト構造。

デメリットは、互換性の問題がちょくちょく起きること。Next.jsの新機能がリリースされても、OpenNext側の対応が追いつくまでは使えない。next のバージョンを上げたら @opennextjs/cloudflare が壊れる、みたいなことが実際にあった。

振り返り

Vercelなら5分で終わる作業に、自分は丸一日かけた。非効率に見えるけど、その過程でWorkersのランタイム制限、エッジコンピューティングの特性、バンドルサイズの最適化手法を体感できた。ドキュメントを読むだけでは身につかない知識が多かった。

インフラエンジニアが個人サイトを持つなら、あえて楽じゃない構成を選ぶのは悪くない選択だと思う。

関連記事

SharePost

他の記事