😊

Cloudflare PagesにNext.jsをデプロイするとSSRが動作するようになったのでどうやって実現されたのかを調べた

2022/11/06に公開

https://developers.cloudflare.com/pages/framework-guides/deploy-a-nextjs-site/

これまでの問題

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のようなサーバーサイドで動作する独自のコードを定義できる

https://developers.cloudflare.com/pages/platform/functions/

デプロイ時に_worker.js という特権ファイルを定義するとこの機能を丸ごと拡張できPagesにきたリクエストに任意のコードを実行できる。

そして、@cloudflare/next-on-pagesというビルドツールが新規に開発されNext.jsプロジェクトを解析して _worker.js とCloudflare Workersにデプロイする各エンドポイントを生成してくれるようになった。

https://blog.cloudflare.com/next-on-pages/

@cloudflare/next-on-pagesの仕組み

  1. Next.jsアプリケーションを一旦npx vercel buildしてBuild Output API (v3)形式に書き出す
  2. ページ単位のFunctionとMiddlewareのJSコードをCloudflare Workers用に書き換える
  3. _worker.jsのテンプレートにnext-on-pages/templates/_worker.js上記をesbuildで差し込んで上書きする
  4. できあがった .vercel/output/static/ をCloudflare Pagesにデプロイする
  5. 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__に注入された各ページやミドルウェアの実体が入っている。

https://github.com/cloudflare/next-on-pages/blob/4858ac4b0597327408444af7d00657c91db17fe4/templates/_worker.js/index.ts#L129-L161

https://github.com/cloudflare/next-on-pages/blob/4858ac4b0597327408444af7d00657c91db17fe4/src/index.ts#L442-L462

esbuildで_worker.jsを吐き出す部分

https://github.com/cloudflare/next-on-pages/blob/4858ac4b0597327408444af7d00657c91db17fe4/src/index.ts#L464-L480

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未対応

https://github.com/cloudflare/next-on-pages/issues/9

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に対応した

https://blog.cloudflare.com/pages-function-goes-ga/

脚注
  1. RFC: Switchable Next.js Runtime https://github.com/vercel/next.js/discussions/34179 ↩︎

Discussion