Cloudflare PagesにNext.jsをデプロイするとSSRが動作するようになったのでどうやって実現されたのかを調べた
これまでの問題
Next.jsのEdge RuntimeはAPI RoutesやMiddlewaresのような単純なリクエスト/レスポンス変換を行う用途で提供されていてReact Componentをレンダリングする(SSR)にはNode.jsランタイム(主にNodeのStreams API)が必要だった[1]。
その上でCloudflare Workersの実行環境でSSRを実現するにはFastly Compute@EdgeのコンポーネントのようにNode.js APIの互換性問題を解決しプラットフォームに適合したグルーコードを生成することが要求された(fastly/next-compute-jsの内部アーキテクチャを調べるを参照)。
なのでCloudflare WorkersにAPI単体をデプロイ+Cloudflare Pagesにエクスポート済みの静的サイトをデプロイしてSPAで動かすというアーキテクチャが一般的だった。
どうやって解決されたか
Cloudflare PagesにバックエンドにCloudflare Workersを使ったFunctionsという機能があってHTTP Middlewareのようなサーバーサイドで動作する独自のコードを定義できる
デプロイ時に_worker.js という特権ファイルを定義するとこの機能を丸ごと拡張できPagesにきたリクエストに任意のコードを実行できる。
そして、@cloudflare/next-on-pagesというビルドツールが新規に開発されNext.jsプロジェクトを解析して _worker.js
とCloudflare Workersにデプロイする各エンドポイントを生成してくれるようになった。
@cloudflare/next-on-pagesの仕組み
- Next.jsアプリケーションを一旦
npx vercel build
してBuild Output API (v3)形式に書き出す - ページ単位のFunctionとMiddlewareのJSコードをCloudflare Workers用に書き換える
- _worker.jsのテンプレートにnext-on-pages/templates/_worker.js上記をesbuildで差し込んで上書きする
- できあがった
.vercel/output/static/
をCloudflare Pagesにデプロイする - Cloudflare Pagesプラットフォーム内部でビルドされサーバーサイド処理がWorkersになりPagesからの呼び出しがService bindingsに割り当てられる
Build Output API (v3)形式に書き出す
npx @cloudflare/next-on-pages
コマンドを実行すると内部的に vercel build
も走る。SSRページ1つAPI Routes 1つのアプリケーションだとWorkersは2つデプロイされるので以下のようになる。
❯ tree .vercel/
.vercel/
├── output
│ ├── builds.json
│ ├── config.json
│ ├── functions
│ │ ├── api
│ │ │ └── hello.func
│ │ │ ├── index.js
│ │ │ └── index.js.map
│ │ └── index.func
│ │ ├── index.js
│ │ └── index.js.map
│ └── static
│ ├── 404.html
│ ├── 500.html
│ ├── _next
│ │ ├── __private
│ │ │ └── trace
│ │ └── static
│ │ ├── KPCaSdMfmvbYN6OWcARe0
│ │ │ ├── _buildManifest.js
│ │ │ └── _ssgManifest.js
│ │ ├── chunks
│ │ │ ├── framework-ed075df0e0b45174.js
│ │ │ ├── main-705eabde1bd26a5d.js
│ │ │ ├── pages
│ │ │ │ ├── _app-b883af44b775b5e6.js
│ │ │ │ ├── _error-e4f561a102d9bb14.js
│ │ │ │ └── index-429a9bc96642cf78.js
│ │ │ ├── polyfills-c67a75d1b6f99dc8.js
│ │ │ └── webpack-4e7214a60fad8e88.js
│ │ └── css
│ │ └── ab44ce7add5c3d11.css
│ ├── _worker.js
│ ├── favicon.ico
│ └── vercel.svg
└── project.json
13 directories, 23 files
Cloudflare Workers用の書き換え処理
Workersエントリーポイント。__FUNCTIONS__や__MIDDLEWARE__に注入された各ページやミドルウェアの実体が入っている。
esbuildで_worker.jsを吐き出す部分
SSRのWorkerのメタ情報。v8-worker
というターゲットが定義されている。
❯ cat .vercel/output/functions/index.func/.vc-config.json
{
"runtime": "edge",
"name": "index",
"deploymentTarget": "v8-worker",
"entrypoint": "index.js",
"envVarsInUse": [
"NEXT_PRIVATE_MINIMAL_MODE"
],
"assets": []
}
感想
これが可能になる前提としてNext.jsのEdge Runtimeで書き出したReactServerのStreams APIを使ったレンダリングをEdge Workerの実行環境がサポートしている必要がある。
Cloudflare Workersの人たちはこれをポリフィル埋め込みではなく自前でサポートしてしまって現在はstreams_enable_constructorsとtransformstream_enable_standard_constructorのフィーチャーフラグをオプトインすると試すことができるというステータス。
今後React ComponentのSSRがEdge Runtimeでサポートされるようになったら、VercelのインフラでもページのSSRやRSCも、現在のAWS Lambdaベースの環境からCloudflare Workersベースの実行環境に移るのではないかと思った。
既知の問題:(1) Next.js v13未対応
v12で試す必要がある。以下は動作を確認した時のdependencies。Wranglerもv2系が必須。
{
"name": "cfpages",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"wrangler:dev": "npx @cloudflare/next-on-pages && npx wrangler pages dev .vercel/output/static --compatibility-flags=streams_enable_constructors",
"deploy": "npx @cloudflare/next-on-pages --experimental-minify && npx wrangler pages publish .vercel/output/static",
"lint": "next lint"
},
"dependencies": {
"next": "^12.0.7",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@cloudflare/next-on-pages": "^0.1.0",
"@cloudflare/workers-types": "^3.18.0",
"@types/node": "18.11.9",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.8",
"eslint": "8.27.0",
"eslint-config-next": "^12.0.7",
"typescript": "4.8.4",
"vercel": "^28.4.14",
"wrangler": "^2.1.15"
}
}
既知の問題:(2) Middlewaresの動作はあやしい
ページコンポーネントのSSRをEdge Runtimeを動かして更にMiddlewaresも組合せるというのがNext.jsの通常のパスにはなさそうなので併用すると空レスポンスに置き換わったりする。
既知の問題:(3) デプロイしたPages Functionsどこ……
内部的にデプロイされたWorker(Function)はまだコンソールから見ることができない。
wranglerにも対応したコマンドはないので不明。
追記:(1) v13に対応した
-
RFC: Switchable Next.js Runtime https://github.com/vercel/next.js/discussions/34179 ↩︎
Discussion