Cloudflare WorkersにNext.jsをデプロイして詰まったポイントまとめ
このサイトは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.json の scripts にまとめておくと楽。
{
"scripts": {
"build": "next build",
"build:worker": "opennextjs-cloudflare build",
"deploy": "opennextjs-cloudflare deploy"
}
}
wrangler.jsonc の設定も必要。自分の場合は main にOpenNextが生成するWorkerのエントリポイントを指定して、compatibility_flags に nodejs_compat を追加した。これがないとNode.js由来のAPIが一切使えなくてビルドが通らない。
ハマったポイント
next/ogが動かない
一番ハマったのがOG画像の動的生成。next/og の ImageResponse は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は使えない。path や crypto は使えるけど、すべての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-matter と marked と shiki(シンタックスハイライト)を全部ランタイムバンドルに含めようとして上限を超えた。対策は単純で、これらを devDependencies に入れてビルド時だけ使う設計に変えた。ランタイムではビルド済みのHTMLを返すだけだから、これらのパッケージは不要。
next.config.ts の serverExternalPackages で除外する方法もあるけど、Workers環境では除外したパッケージが存在しないのでエラーになる。ビルド時に完結させる方が確実。
wrangler.jsoncの罠
地味にハマったのが wrangler.jsonc の設定。compatibility_date を古い日付にしていると、一部のAPIが使えなかったり挙動が変わったりする。自分は最初 2024-01-01 にしていて、Response.json() の挙動が微妙に違うことに気づくのに30分くらいかかった。
基本的には compatibility_date は可能な限り新しい日付にしておくのが無難。
デプロイの流れ
実際のデプロイフローはシンプル。
npm run buildでNext.jsビルド + ブログJSON生成npx opennextjs-cloudflare buildでWorkers向けに変換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のランタイム制限、エッジコンピューティングの特性、バンドルサイズの最適化手法を体感できた。ドキュメントを読むだけでは身につかない知識が多かった。
インフラエンジニアが個人サイトを持つなら、あえて楽じゃない構成を選ぶのは悪くない選択だと思う。
関連記事
他の記事
ブラウザ横スクロールアクションゲーム開発記|TypeScript×Canvasで作るネオンランナー
HTML5 CanvasとTypeScriptで横スクロールアクションゲーム「ネオンランナー」を自作した。物理エンジン、衝突判定、操作性の工夫を解説する。
ブラウザで遊べるテトリス風パズルをTypeScriptで作った話
HTML5 CanvasとTypeScriptでテトリス風ブロックパズルを実装した。エンジンとUIを分離するSnapshotパターンや、ネオン演出のこだわりを解説する。
ゲーム実況のサムネイル作りで学んだデザインの基本
非デザイナーがゲーム配信のサムネイルを自作し続けて気づいた、視認性・配色・構図の基本ルール。試行錯誤の記録。
配信のコメント欄が動いた瞬間の話。ゼロからイチの体験
ゲーム配信でコメントがゼロの日々を超えて、初めてリアルタイムでコメントが来た瞬間の話。ゼロからイチの体験がモチベーションを変えた。