Open21

@cloudflare/next-on-pages 読む

ピン留めされたアイテム
naporitannaporitan

メンテナのデモ
https://next-cache-demo.eli.cx/

naporitannaporitan

少し前に試して動かなかった middleware は今動くようになっているらしい

コードを見て重点的に確認すること

  • fetch
    • revalidate
    • tags
      • revalidateTag
  • cache
  • Cache API なのか KV なのか
  • images
    • custom loader を cloudflare stack でやるとしてどういう選択肢があるのか
naporitannaporitan

fetch

Next.js も同じだけどまず fetch に patch を当てるところから始まる。

https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/_worker.js/index.ts#L25

blob:https://INTERNAL_SUSPENSE_CACHE_HOSTNAME.local/v1/suspense-cache/ から始まる request 以外は globalFetch に流している。

さらっと見てる感じデフォルトの fetch では vercel で cache してくれる next の option に関しては検証してなさそう。

fetch('xxxx', {
  next: {
    revalidate: 3600,
    tags: ['next']
}

blob: から始まる URLについて

コメントには 例えば @vercel/og に使われると書いてあるが、 build 成果物として .bin になるようなケースって存在するんだろうか?
wasm を import するとインライン展開されて blob://xxx.wasm とかになるんかな....?(要調査

https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/_worker.js/utils/fetch.ts#L52-L54

plugins が .bin で展開されているのかも
https://developers.cloudflare.com/pages/functions/plugins/

INTERNAL_SUSPENSE_CACHE_HOSTNAME.local が含まれる URL について

RSC のキャッシュなんだと思うが、Suspense ってでかめの名前を付けているので Streaming されるコンポーネントがすべてキャッシュされそう。

GET は cache があれば返す。なければ 404, POST は revalidateTag を付けたうえで body を cache する。

/v1/suspense-cache/revalidate に対してリクエストがあったときは method にかかわらず searchParameter の tags をみて revalida する。

@cloudflare/next-on-pages における SuspenseCache について

SuspenseCache は KV, Cache API がコード中には出てくるが Interface はすべて CacheAdapter に従っているのでここだけ見ておけばよい。

ここでは cache purge の仕組みは tags と revalidate のみで管理されている。(revalidate は使われてない?)
https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/cache/adaptor.ts#L218-L232

KV と Cache API の update method をそれぞれ乗せておく。KV は revalidate 見てないので自動で揮発しないらしいがこれはいいのか?

https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/cache/kv.ts#L17

https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/cache/cache-api.ts#L19-L32

naporitannaporitan

images

next/image を使用すると custom loader の設定をしていなければ /_next/image 以下から画像が配信される。vercel では /_next/images 以下で画像にアクセスするときには squoosh が仕込まれていてリサイズがされるが @cloudflare/next-on-pages ではどうなるだろうか?

ここで行われている
https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/_worker.js/index.ts#L51-L56

それぞれ見ていく

  • handleImageResizingRequest
  • env.ASSETS
  • BUILD_OUTPUT
  • CONFIG.images

handleImageResizingRequest

url から resize option と切り出したり Accept header から format を取り出している
https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/_worker.js/utils/images.ts#L153

自分自身だったら env.ASSETS.fetch を使ってそうでないなら fetch を利用
https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/_worker.js/utils/images.ts#L163-L166

next.config.mjs から response header を付けてあげる
https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/_worker.js/utils/images.ts#L107-L140

env.ASSETS

CloudflarePages 専用の service binding。自分自身から静的アセットを取り出すときに使う。
https://developers.cloudflare.com/pages/functions/api-reference/#envassetsfetch

BUILD_OUTPUT

build ディレクトリのファイルを見れるものだと思う

CONFIG.images

next.config.mjs の中身だと思う

naporitannaporitan

リサイズも画像の format も変わってないじゃん!

せっかくデータとして持ってるのに使ってない回
https://github.com/cloudflare/next-on-pages/blob/a84add8db159a7c5d728c8169ef7b5aeac768e29/packages/next-on-pages/templates/_worker.js/utils/images.ts#L90

素直に custom loader と CloudflareImages の Image Transform 単体か ImageTransform + Cloudflare Worker + WorkerRoute を使用しましょうね案件でした。

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

naporitannaporitan

読むのにめちゃくちゃ時間がかかる上に Next.js が吐き出すファイル構造まで知ってなきゃいけないので地力はあきらめた。

handleRequest の内部では RoutesMatcher findMatch generateResponse 関数が実行されていて、 findMatch は matcher から今回使用する route を決定するやつ、generateResponse は route を Response に変換する関数。RoutesMatcher がかなり巨大なのと build した後のファイル構造も詳しく知っている必要があったためざっくり挙動を GPT-4 に聞いた。

このコードは、Cloudflare Pages上でNext.jsのAppRouteを動かすために使用される`@cloudflare/next-on-pages`ランタイムの実装部分です。主に、リクエストが来た時にどのルートにマッチするかを判断し、対応するミドルウェアを実行したり、適切なレスポンスを生成する流れを定義しています。ここでは、その処理の流れとRSCに関する言及を説明します。

### 処理の流れ

1. **コンストラクタ (`constructor`):** 
   - リクエストのURL、クッキー、パスなどの初期化。
   - ワイルドカードマッチの設定。
   - ロケール情報のセットアップ。

2. **ルートのマッチング (`checkRouteMatch`):**
   - リクエストのパスとルートの`src`属性をPCRE(Perl Compatible Regular Expressions)でマッチング。
   - HTTPメソッド、`has``missing`、ステータスコードの条件を満たすかチェック。
   - マッチした場合、そのルート情報を返す。

3. **ミドルウェアの実行 (`runRouteMiddleware`):**
   - 指定されたパスに対応するミドルウェアがあれば実行。
   - ミドルウェアからのレスポンスを処理し、必要に応じてリクエストヘッダーやパスを上書き。

4. **ルートの適用 (`applyRouteOverrides`, `applyLocaleRedirects`, `applyRouteHeaders`, `applyRouteStatus`, `applyRouteDest`):**
   - ルートに定義されたオーバーライド、リダイレクト、ヘッダー、ステータスコード、デスティネーションの適用。

5. **フェーズのチェック (`checkPhase`):**
   - `none``miss``hit``error`といった異なるフェーズでルートのマッチングを試みる。
   - 各フェーズでルートを順番にチェックし、マッチするものがあれば処理を実行。
   - ルートの処理結果に基づき、次のフェーズへ移動するか、処理を終了する。

6. **マッチングの実行 (`run`):**
   - 指定されたフェーズからマッチング処理を開始し、結果を返す。

### RSC (React Server Components) について

RSCは、サーバー側でのみ実行されるReactコンポーネントで、クライアントサイドでのJavaScriptの実行を減らすことでパフォーマンスを向上させることができます。このコード内では、`.rsc`の拡張子を持つパスに特別な扱いをしている部分があります。特に、`applyRouteDest`メソッド内で、リクエストのパスがRSC関連のものであるかどうかをチェックし、処理を調整しています。

- **RSC Indexルートの特別な扱い:** `/index.rsc`への書き換えを試みますが、元のパスが`/`(ルート)以外の場合や、`/__index.prefetch.rsc`へのリクエストの場合は、その書き換えを行いません。

- **RSCファイルがビルド出力に存在しない場合の処理:** パスに`.rsc`拡張子が含まれるが、該当するR

SCページがビルド出力に存在しない場合、その拡張子を削除します。

これらの処理により、RSCに関する特別なケースを適切に扱いつつ、リクエストを正確にルーティングできるようにしています。
naporitannaporitan

request path に対応するファイルを next build が吐き出すディレクトリにマッチさせて middleware, RSC, static file を実行して Response を返しているということくらいしかわからなかった。

ただ cache 周りが書いてないことだけは分かった。

naporitannaporitan

じゃあ fetch に対して patch を当てていたあのコードたちはどういうことなんだろうか?

Suspense Cache をCacheAPI で使えるようにする PR
https://github.com/cloudflare/next-on-pages/pull/419
Suspense Cache を KV でも使えるようにする PR
https://github.com/cloudflare/next-on-pages/pull/548

CacheAPI のほうの PR を読んでいると export const SUSPENSE_CACHE_URL = 'INTERNAL_SUSPENSE_CACHE_HOSTNAME'; は Next.js で使われている環境変数であるといわれている。
Next.js 側から RSC を fetch するときはこの hostname が刺されるってことか
https://github.com/cloudflare/next-on-pages/pull/419#discussion_r1289992359

naporitannaporitan

Next.js は fetch を patch している

多分これ
https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/patch-fetch.ts#L179

Next.js から fetch をたたくと

  1. fetch (Next.js
  2. staticGenerationStore 経由で IncrementalCache がたたかれる (Next.js
  3. FetchCache (Next.js
  4. handleSuspenseCacheRequest (@cloudflare/next-on-pages

staticGenerationStoreAsyncLocalStoragefetch.__nextGetStaticStore() から刺されるのだが、呼ばれているファイルが複数だったり AsyncLocalStorage が wrap されたもののあったりして完全に追えなかった。

https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/patch-fetch.ts#L228-L230

キーワード

  • staticGenerationAsyncStorage
  • staticGenerationAsyncStorageWrapper
  • requestAsyncStorage
  • RequestAsyncStorageWrapper
  • __nextGetStaticStore
naporitannaporitan

patch-package で挙動を確認しながらいつ cache が入っているか見てみる

cache が使われるときと保存されるタイミングに一貫性がなくて困惑....

Cache API を使用しているときはたまに使われる(Cloudflare の Cache API ってこんなに cache.match 不安定じゃないよな....

naporitannaporitan

外部リクエストには fetch(url, { next: { } }) で cache されるし、revalidateTag revalidatePath が動く。
内部リクエストに関しては unstable_cache がないと動かない。

@cloudflare/next-on-pages の fetchPatch は効いていたので、next.js 側の fetchPatch がはがれていそうな雰囲気。
suspense-cache のリクエストが発火してなかったのでかなり怪しい

fetch -> IncrementalCache -> FetchCache -> handleSuspenseCacheRequest

next.js の patch が効いてないと fetch から FetchCache までたどり着かないのでそれで動かないんじゃないかなぁと予想してる

naporitannaporitan

cache が使われるときと保存されるタイミングに一貫性がなくて困惑....
Cache API を使用しているときはたまに使われる(Cloudflare の Cache API ってこんなに cache.match 不安定じゃないよな...

これがなぜかわかった。一度 revalidataTag をするとすべての fetch が cache されなくなり、deploy するたびに cache されるという挙動をしていたんだが、これは inmemory で revalidatedTags という revalidateTag されたときに登録する変数がいるのだが、こいつの状態がクリアされたり、リクエストが来なくなると worker インスタンスが消えて inmemory 状態が揮発するからだった

revalidatedTags が破棄されない理由も、KV の put が終わらないからという奇妙な理由だった。
getRequestContext から得られる executeCtx.waitUntil をかませることで put を待たないようしたら動作するように見えたが、waitUntil すると cache が更新される前に rerender されることがありなそうなので要調査