🔥

Next.jsのFull Route Cache周りについて調べる

2025/02/13に公開

はじめに

Full Route Cacheは静的ページをCacheしてくれるものだと思いますが、どんなときにどんなふうにCacheが効くのか調べます
RSC Payloadとサーバーで生成したHTMLをキャッシュして早く表示させる機能らしいです。
まだ良くわかっていないので実際に手を動かして調べてみようと思います。

準備

Next.jsの立ち上げ

最新で立ち上げる

pnpm dlx create-next-app@latest

srcに以下が作られる。

- src
  - app
    - favicon.ico
    - globals.css
    - layout.tsx
    - page.tsx
  • わかりやすくページを編集する

const Page = () => {
  const date = new Date().toISOString()
  return (
    <>
      <h1>test</h1>
      <p>{date}</p>
    </>
  )
}

export default Page

  • 各項目でBuildとStartを実行します
    • next devを使うとnext startとは違う挙動になるので注意
      • すべてno-store状態になるようだ

検証

静的ルートかつフェッチ無し

pnpm build
pnpm sart

上記を実行して確認する

> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (5/5)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ○ /                                    137 B           105 kB
└ ○ /_not-found                          978 B           106 kB
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)  prerendered as static content
  • ◯ (Static)と記載あるものはFull Route Cacheが効いている
    • 静的ページはBuildされてCacheが効いている
HTTP/1.1 304 Not Modified
vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-stale-time: 4294967294
X-Powered-By: Next.js
Cache-Control: s-maxage=31536000, 
ETag: "wih7ejzr0r3wx"
Date: Tue, 11 Feb 2025 09:10:43 GMT
Connection: keep-alive
Keep-Alive: timeout=5
  • 以下の項目を見るとCacheされている
    • x-next-js-cache: HIT
    • Cache-Control: s-maxage=31536000
    • Cacheが効いていて、日時はBuild時の状態から更新しても変わらない

静的ルートかつ動的設定

export const dynamic = 'force-dynamic'
  • page.tsxかlayout.tsxに追記でき、呼び出した関数すべてでdynamicになる
> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (4/4)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ƒ /                                    137 B           105 kB
└ ○ /_not-found                          978 B           106 kB
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand
  • トップページを見ると、Dynamicになっていることがわかる
    • f (Dynamic)に変わっています。
    • no-cacheになっていて、ページを読み込むたびに日時が変わる

静的ルートかつrecalidate

export const revalidate = 10
  • page.tsxに上記の1行を付け足します
> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (5/5)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ○ /                                    140 B           105 kB
├ ○ /_not-found                          978 B           106 kB
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)   prerendered as static content
  • Staticとして吐き出された
    • 10秒で日時が変更された
      • おそらく、初回アクセスがBuild時からrevalidate秒分立っていなかったらBuild時の値が表示される

静的ルートかつフェッチあり(設定なし)

const data = await fetch("https://api.test/")
  • page.tsx内でフェッチ処理がある場合
> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (5/5)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ○ /                                    140 B           105 kB
├ ○ /_not-found                          978 B           106 kB
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)   prerendered as static content
  • Staticとして吐き出された
    • Build時にフェッチし、Cacheされる
    • 更新してもBuild時の日時から変更はない

静的ルートかつフェッチあり(no-store)

const data = await fetch("https://api.test/", { cache: "no-store" }))

  • page.tsxの中にフェッチ処理を定義し、cacheをno-storeにする
> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (7/7)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ƒ /                                    140 B           105 kB
├ ○ /_not-found                          978 B           106 kB
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand
  • Dynamicとして吐き出された
    • ページにアクセスするたびにフェッチが走り、日時も更新される

静的ルートかつフェッチあり(force-cache)

const data = await fetch("https://api.test/", { cache: "force-cache" }))
  • page.tsxの中にフェッチ処理を定義し、cacheをforce-cacheにする
> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (7/7)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ○ /                                    140 B           105 kB
├ ○ /_not-found                          978 B           106 kB
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)  prerendered as static content
  • Staticとして吐き出された
    • Build時にフェッチされ、Build時の日時が表示されたまま変わらない

動的ルート

const Page = async ({ params }: {params: Promise<{id: string}>}) => {
  const { id } = await params
  const date = new Date().toISOString()
  return (
    <>
      <h1>test</h1>
      <p>{id}:{date}</p>
    </>
  )
}

export default Page
  • app/[id]/page.tsxを定義し、静的ルートの時と同じように日時を見て検証する
> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (5/5)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ○ /                                    140 B           105 kB
├ ○ /_not-found                          978 B           106 kB
└ ƒ /[id]                                140 B           105 kB
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand
  • /[id]の部分がDynamicとして吐き出されていることが分かる
    • 日時はアクセスのたびに更新される

動的ルートかつgenerateStaticParams

export async function generateStaticParams() {
  return [{id: "1"}, {id: "2"}]
}
  • page.tsxの中にgenerateStaticParamsで[id]の内容に定義する
> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (7/7)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ƒ /                                    140 B           105 kB
├ ○ /_not-found                          978 B           106 kB
└ ● /[id]                                140 B           105 kB
    ├ /1
    └ /2
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses generateStaticParams)
ƒ  (Dynamic)  server-rendered on demand
  • SSGとして[id]が吐き出された
    • idが1,2についてはBuild時の時間が表示された
    • それ以外については、
      • 初回はno-cacheとなり、MISSとなる
      • アクセスした日時が表示されその後は日時の変更が無くキャッシュされる

動的ルートかつフェッチあり(no-store)かつgenerateStaticParams

> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (7/7)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ○ /                                    140 B           105 kB
├ ○ /_not-found                          978 B           106 kB
└ ● /[id]                                140 B           105 kB
    ├ /1
    └ /2
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)  prerendered as static content
●  (SSG)     prerendered as static HTML (uses generateStaticParams)
  • SSGとして吐き出された
    • アクセスのたびに日時は更新される
      • (Buildは意味ないものになるのだろうか?それとも枠だけ作られるのか?)

動的ルートかつheadersやcookie

import { headers } from "next/headers"

const Page = async () => {
  const header = await headers()
  const getXUrl = header.get("x-url")
  console.log("header", getXUrl)
  const date = new Date().toISOString()

  return (
    <>
      <h1>test</h1>
      <p>{date}</p>
    </>
  )
}

export default Page
  • v15から動作が変わったheadersを入れて検証する
> next build

   ▲ Next.js 15.1.7

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (5/5)
 ✓ Collecting build traces
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ƒ /                                    140 B           105 kB
├ ○ /_not-found                          978 B           106 kB
└ ƒ /[id]                                140 B           105 kB
+ First Load JS shared by all            105 kB
  ├ chunks/79b3e8d8-b7e8f2abc8f228bc.js  52.9 kB
  ├ chunks/919-ec2d758a7b1885c4.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand
  • Dynamicとして吐き出される
    • no-cacheとなり、アクセスのたびに日時が更新される

終わり

まだまだパターンは多かったり、組み合わせでどちらが優先されるか気になるところではあるが、
実際にBuildを繰り返してみて
動的な要素(fetch、headers, cookie)等がある場合はFull Route Cacheされない
という動作が少しわかりました。
つまり、静的ページをより早く表示させるための機能という解釈を個人的にはしました。

Discussion