Next.jsでのキャッシュ戦略
Next.js v15.3.5時点でのキャッシュ戦略についての学習メモです。
Next.jsのキャッシュは非常に強力だがかなり複雑なためドキュメントと自分の理解を記していく。
Next.jsにおいてキャッシュは大きく分けて4つある。これらを組み合わせることでパフォーマンスを向上させたりコストを削減したりしている。

Request Memoization
Next.jsはfetchAPIを拡張して、同じURL・同じオプションを持つリクエストを自動的にメモ化する。そのため、コンポーネントツリーでいくつも同じリクエストをしていたとしても実際には同じリクエストは1回しか実行されない。

複数箇所でリクエストを実行していても、Next.jsはそれをまとめて最小になるようにリクエストするよ という図。
この仕組みがあるので、例えばレイアウトと子コンポーネントで同じfetchの結果を使いたい時にpropsで渡したり状態管理で渡す必要がなくなる。一見同じfetchを実行していて無駄が多そうでも実はキャッシュが効いているので効率的と言える。
キャッシュする流れ

最初にfetchリクエストが呼び出された時、キャッシュを確認するがまだ保存されていないためMISSとなる。そしてデータソース(またはデータキャッシュ)にアクセスしデータを返す。この時、メモリーにデータを保存する。
同じfetchリクエストが呼ばれた時、1で保存したキャッシュがあるためこれを返す。
このfetchを拡張したメモ化はGETメソッドのみに適用される。POSTでデータ取得をおこな場合には適用されない。
Data Cache
こちらはサーバーリクエストやデプロイを跨いでデータフェッチの結果を永続化することのできるキャッシュである。Request Memoizationはレンダーごとのキャッシュだがこちらはより広範囲のキャッシュといえる。
キャッシュする流れ

レンダリング中に「force-cache」オプションが指定されたリクエストが呼び出されると、それに対応したキャッシュがないかチェックを行う。
Request Memoizationにあればそこから返す
もし、キャッシュされたレスポンスがなければデータソースに対してリクエストが行われる。その結果はData CacheとRequest Memoizationに保存される。
また、「no-store」オプションが指定されていた場合はキャッシュが存在してもデータソースに対してリクエストを行う。
キャッシュ期間
Data Cacheはrevalidateの期間内は間でデプロイを挟んでもキャッシュされ続ける。
revalidate
revalidateは時間ベースの設定とオンデマンドの設定の2種類を使い分けることができる
時間ベース
各fetchリクエストにrevalideの時間を設定することで機能する。Route Segment Configでも設定ができるのでこの辺りはISRの設定と同じといえる。
// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })
初回のリクエストはデータソースにアクセスし、その結果がData Cacheに保存される
revalidateの時間内に呼ばれたリクエストはData Cacheから返す
revalidateの時間外に呼ばれたリクエストはキャッシュされた古いデータを返す
時間外のリクエストが来るとStale状態になり、バックグラウンドでデータソースにアクセスをしキャッシュを更新する
オンデマンドベース
こちらはrevalidatePathやrevalidateTagを使った更新方法である。

初回のリクエストはデータソースにアクセスし、その結果がData Cacheに保存される
revalidatePath等が呼ばれるとキャッシュは即時削除される次にリクエストが行われるとData CacheはMissとなり、データソースに再びアクセスを行う
Full Route Cache
Next.jsはビルド時やrevalidate時にルートをレンダリングしてキャッシュする。これにより、リクエストごとにサーバーでレンダリングする代わりにキャッシュされたルートを提供することができ、ページの読み込みを高速にすることができる。
Full Route Cacheは次の仕組みで行われる。
サーバー上でのReactレンダリング
サーバー上でNext.jsはReactのAPIを使ってレンダリングを行う。レンダリング作業は個々のルートセグメントとサスペンス境界によってチャンクごとに分割される。
このチャンクは2段階に分けてレンダリングが行われる
ReactはサーバーコンポーネントをReact Server Component Payload(以下RSC Payload)というストリーミングに適したデータ形式にレンダリングされる
Next.jsはRSC PayloadとClient Component JSからHTMLを生成する
📝RSC Payloadとは? React Server Componentsのレンダリング結果を含むコンパクトなバイナリ形式のもので以下のものを含む
Server Componentsのレンダリング結果
Client Componentsがレンダリングされる予定の場所のプレースホルダーとJSの参照
Server ComponentからClient Componentに渡されるpropsの情報
サーバー上でのNext.jsのキャッシュ
Next.jsはデフォルトでRSC PayloadとHTMLをサーバーにキャッシュする。タイミングはビルド時またはrevalidate時にStatic Renderingしたルートに適用する。
クライアント上でのHydrationとReconciliation
クライアント側では次のステップでキャッシュされているHTMLやRSC Payloadを利用する。
まずキャッシュされたHTMLを使って初期表示を高速に行う
RSC Payloadを使用して、クライアント側でReactツリーを再構築する。そして現在表示されているHTMLと照合して必要に応じてDOM更新を行う。
Hydrationを行い、インタラクティブな機能を現在のHTMLに追加していく
クライアントでのNext.jsのキャッシュ(Router Cache)
RSC PayloadはクライアントサイドのRouter Cacheに保存される。(Router Cacheは下のセクションで説明予定) すでに訪れたルートのキャッシュは保存されており、また将来のルートをプリフェッチする。
後続のナビゲーション
Next.jsはナビゲーションやプリフェッチの際にRSC Payloadがルーターキャッシュに保存されているかどうかを確認する。保存されていればサーバーへの新たなリクエストを行わない。キャッシュに保存されていなければサーバーからRSC Payloadを取得し、キャッシュに詰める。
静的レンダリングと動的レンダリングのキャッシュの違い
ビルド時にルートがキャッシュされるかどうかはレンダリング方式によって変わってくる。静的レンダリングではビルド時にキャッシュされるが動的レンダリングではリクエスト時にレンダリングされる。
最初のページ遷移において、動的レンダリングはFull Route CacheにはキャッシュがSETされないことがわかる。

Full Route Cacheを無告にしてもデータキャッシュには影響しない。これによりキャッシュされたデータとキャッシュされてないデータの両方を持つルートを動的にレンダリングすることができる。
Client-side Router Cache
Next.jsはクライアント側にインメモリのルーターキャッシュを持ち、RSC Payloadをレイアウト、ローディング状態、ページごとに分割して保存する。ユーザーが閲覧したルートはキャッシュし、次に訪れそうなルートはプリフェッチを行う。
キャッシュはブラウザの一時メモリに保存される。期間は二つの要素によって決まる。
Session:キャッシュはナビゲーションの期間持続する。正し、ページ更新時にクリアされる。
Automatic Invalidation Period:レイアウトとロード状態のキャッシュは特定の時間が経過すると自動的に無効になる。この期間はリソースがどのようにプリフェッチされたかとリソースが静的に生成されたかによって異なってくる。
デフォルトでは動的ページはキャッシュされず、静的ページは5分で無効になる。
NextLinkのprefetchをtrueに設定(もしくはrouter.prefetch)を設定すると動的も静的も5分になる。