🐧

Next.js v15.0.3における4つのキャッシュ挙動調査まとめ

2024/11/25に公開

⚠️注意点

  • Next.jsのcanaryバージョンである、experimental: dynamicIOや、"use cache"はOFFの状態です
  • local環境で、next build & next startをした時の挙動です(デプロイ後の挙動は未調査です)
  • 少し雑なまとめです(ファイル名等の命名やGIFも雑です🙏)

Data cache

fetch単位でのキャッシュ
初めてリクエストすると、リクエストとデータをキャッシュします。2回目以降のリクエストでキャッシュが見つかる場合は、リクエストせずにキャッシュデータを返す
キャッシュされていないデータソースは、常にfetchして取得する
Next.jsは、fetchした結果をData Cacheとして保存します。fetchによりデータソースにリクエストした結果はData Cacheに保存され、以降同一リクエストはData Cacheから返される
Data Cacheは永続的で複数デプロイ間にわたって有効なので、再デプロイしてもキャッシュは破棄されない。なので一定期間や特定のタイミングでキャッシュを破棄するためにfetchオプションなどによる再検証が用意されている

オプトイン・オプトアウト

オプトイン

  • デフォルトでON
  • fetchオプションにcache: “force-cache”を付与
  • const fetchCache = 'force-cache'を定義

オプトアウト

  • fetchオプションにcache: “no-store”を付与
  • const fetchCache = 'default-no-store'を定義
  • const dynamic = 'force-dynamic'を定義
  • const revalidate = 0を定義

fetchのcacheオプションとbuildの挙動

  • cache: "force-cache"
    • デフォルト
    • fetchをキャッシュする
      • 静的なページであればbuild時にキャッシュする
      • 動的なページであれば初めにページを訪れた時のfetchをキャッシュする
    • revalidatePath, revalidateTagでキャッシュ削除可能
      • その後ページを訪れた時に、再度fetchを実行しキャッシュする
  • cache: "no-store"
    • fetchをキャッシュせずリクエストの度にfetchする(build時にfetchはしない)
    • ページは動的なページとなる
  • cache: "force-cache" + next.revalidate
    • fetchをnext.revalidateで設定した時間キャッシュする
      • 静的なページであれば、初回はbuild時にキャッシュする
      • 動的なページであれば、初回は初めにページを訪れた時にキャッシュする
    • revalidatePath, revalidateTagでキャッシュ削除可能
      • その後ページを訪れた時に、再度fetchを実行しキャッシュする
    • ページは静的なページとなる(他に動的なページとなるコードが無ければ)

ビルド結果

ページ毎のコード
// sc-build-default ページ
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')

// sc-build-force-cache ページ
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1', {
  cache: 'force-cache',
})

// sc-build-revalidate ページ
const res = await fetch(
  'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
  {
    cache: 'force-cache',
    next: {
      revalidate: 5,
    },
  },
)

// sc-build-no-store ページ
const res = await fetch('http://localhost:3000/api/health', {
  cache: 'no-store', // 向き先がRoute Handlersでもエラーにならない 動的なページになる
})

.next/server/app に静的なページのHTMLが生成されることを確認
(sc-build-no-cacheは動的なページなので、ビルド時に生成されない)

ちょい戦いメモ

cacheが有効の状態で、向き先がRoute Handlersだとbuild時にエラー出る問題

→ buildする時に、Next.jsのサーバーは止まっている(Route Handlersは起動していない)からfetchリクエストが失敗する(それはそう)

const page = async () => {
  const res = await fetch('http://localhost:3000/api/health')
  const data = await res.json()
  return (
    <div>
      <h1>sc1</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}

export default page
Error occurred prerendering page "/sc1". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: fetch failed
    at node:internal/deps/undici/undici:13178:13
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async n (/Users/s22489/Documents/project/next-15-test/.next/server/app/sc1/page.js:1:2978)
Export encountered an error on /sc1/page: /sc1, exiting the build.
  Static worker exited with code: 1 and signal: null

✅ 動的ページにする

Route Handlersとの疎通をServer Componentsでしたいなら、動的なページにしてbuild時にfetchを走らせないことでbuild時のエラーを回避できる

+ export const dynamic = 'force-dynamic'

const Page = async () => {
  const res = await fetch('http://localhost:3000/api/health', {
+    cache: 'no-store',
  })
  const data = await res.json()
  return (
    <div>
      <h1>sc1</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}

export default Page

dynamic APIsと、const fetchCacheと、const dynamicと、const revalidateの組み合わせの挙動

  1. dynamic APIs VS cache: “force-cache”
    → 動的なページとなる
    → fetchはbuild時に行われず、初回リクエスト時のfetchをキャッシュする
    → cookie、headers、searchParamsは表示される

  2. dynamic APIs VS const dynamic = 'force-static'
    → 静的なページとなる
    → fetchはbuild時のみ行われキャッシュする
    → cookie、headers、searchParamsは表示されない

  3. dynamic APIs VS const dynamic = 'force-static' VS cache: “no-store”
    → 静的なページとなる
    → fetchはbuild時のみ行われキャッシュする
    → cookie、headers、searchParamsは表示されない

  4. const fetchCache = 'default-no-store'
    → 動的なページになる
    → fetchはリクエストの度行われ、キャッシュはされない

  5. const fetchCache = 'default-no-store' VS cache: “force-cache”
    → 静的なページになる
    → fetchはbuild時のみ行われキャッシュする

  6. const dynamic = 'force-dynamic' VS cache: “force-cache”
    → 動的なページになる
    → fetchはbuild時に行われず、初回リクエスト時のfetchをキャッシュする

  7. const dynamic = 'force-static' VS cache: “no-store”
    → 静的なページになる
    → fetchはbuild時のみ行われキャッシュする

  8. const revalidate = 0
    → 動的なページになる
    → fetchはリクエストの度行われ、キャッシュはされない

  9. const revalidate = 0 VS cache: “force-cache”
    → 動的なページになる
    → fetchはbuild時に行われず、初回リクエスト時のfetchをキャッシュする

ビルド結果

動作

各ページのコード

// 1. data-cache-cookie-force-cacheページ
const Page = async ({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
    {
      cache: 'force-cache',
    },
  )
  const data = await res.json()
  const cookie = await (await cookies()).getAll()
  const header = await headers()
  const params = await searchParams
...
}

// 2. data-cache-cookie-force-staticページ
export const dynamic = 'force-static'

const Page = async ({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Asia/Tokyo',
  )
  const data = await res.json()
  const cookie = await (await cookies()).getAll()
  const header = await headers()
  const params = await searchParams
...
}

// 3. data-cache-cookie-force-static-force-cacheページ
export const dynamic = 'force-static'

const Page = async ({
  searchParams,
}: {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Asia/Tokyo',
    { cache: 'force-cache' },
  )
  const data = await res.json()
  const cookie = (await cookies()).getAll()
  const header = await headers()
  const params = await searchParams
...
}

// 4. data-cache-default-no-storeページ
export const fetchCache = 'default-no-store'

const Page = async () => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
  )
...
}

// 5. data-cache-default-no-store-force-cacheページ
export const fetchCache = 'default-no-store'

const Page = async () => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
    {
      cache: 'force-cache',
    },
  )
...
}

// 6. data-cache-force-dynamic-force-cacheページ
export const dynamic = 'force-dynamic'

const Page = async () => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
    {
      cache: 'force-cache',
    },
  )
...
}

// 7. data-cache-force-static-no-store
export const dynamic = 'force-static'

const Page = async () => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
    {
      cache: 'no-store',
    },
  )
...
}

// 8. data-cache-revalidate-0
export const revalidate = 0

const Page = async () => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
  )
...
}

// 9. data-cache-revalidate-0-force-store
export const revalidate = 0

const Page = async () => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
    {
      cache: 'force-store',
    },
  )
...
}

.next/server/app に静的なページのHTMLが生成されることを確認

以下のページ以外は動的ページなので、ビルド時に生成されません

  • data-cache-cookie-force-static-force-cache
  • data-cache-cookie-force-static
  • data-cache-default-no-store-force-cache
  • data-cache-force-static-no-store

revalidatePath, revalidateTagを実行する挙動(トリガー: Route Handlers)

revalidatePathを実行する時

ページ

  • revalidate-button
    • revalidatePathを発火するボタンを配置したページ
  • revalidate-path1
    • cache: "force-cache"のfetchを含む静的なページ
  • revalidate-path2
    • cache: "force-cache"のfetchを含む静的なページ
Route Handlersのコード

参考

src/app/api/revalidatePath/route.ts

import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'

export async function GET(request: NextRequest) {
  const path = request.nextUrl.searchParams.get('path') || '/'
  revalidatePath(path)
  console.log('🚀 ~ API revalidatePath :', path)
  return NextResponse.json({ revalidated: true, now: Date.now() })
}

src/app/api/revalidateTag/route.ts

import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const tag = request.nextUrl.searchParams.get('tag') || '/'
  revalidateTag(tag)
  console.log('🚀 ~ API revalidateTag :', tag)
  return NextResponse.json({ revalidated: true, now: Date.now() })
}
revalidate-buttonページ

(コード雑です!🙏)

'use client'
import Link from 'next/link'

const Page = () => {
  console.log('🚀 ~ revalidate-button')

  const revalidatePath = async (path: string) => {
    const res = await fetch(
      `http://localhost:3000/api/revalidatePath?path=/${path}`,
    )
    const data = await res.json()
    if (data.revalidated) {
      alert(`${path}をrevalidateしました`)
    }
  }

  const revalidateTag = async (tag: string) => {
    const res = await fetch(
      `http://localhost:3000/api/revalidateTag?tag=${tag}`,
    )
    const data = await res.json()
    if (data.revalidated) {
      alert(`${tag}をrevalidateしました`)
    }
  }

  return (
    <div>
      <h1>revalidate-button</h1>

      <p>revalidateボタン</p>
      <div className='flex flex-col items-start gap-4'>
        <div>
          <button
            className='bg-red-500'
            onClick={() => revalidatePath('revalidate-path1')}
          >
            path1
          </button>
          <Link href='/revalidate-path1' prefetch={false}>
            → Link
          </Link>
        </div>
        <div>
          <button
            className='bg-red-500'
            onClick={() => revalidatePath('revalidate-path2')}
          >
            path2
          </button>
          <Link href='/revalidate-path2' prefetch={false}>
            → Link
          </Link>
        </div>

        <div>
          <button className='bg-red-500' onClick={() => revalidateTag('tag1')}>
            tag1
          </button>
          <Link href='/revalidate-tag1' prefetch={false}>
            → Link
          </Link>
        </div>

        <div>
          <button className='bg-red-500' onClick={() => revalidateTag('tag2')}>
            tag2
          </button>
          <Link href='/revalidate-tag2' prefetch={false}>
            → Link
          </Link>
        </div>
      </div>
    </div>
  )
}

export default Page

挙動

  • revalidatePathの引数のページが以下の動作をする
    • ページ内に存在する、fetch: cache: “force-cache”のキャッシュが破棄される
    • Data Cacheが消えることに連動し、Full Route Cacheも消える
    • ページが再生成される
  • Router Cacheが消えず、revalidatePathを実行した後にリロードしないと再生成されたページを表示できない場合がある

revalidateTagを実行する時

ページ(省略)

  • revalidate-tag1
    • tag: [”tag1”]のfetch
  • revalidate-tag2
    • tag: [”tag2”]のfetch
  • revalidate-tag1-other
    • tag: [”tag1”]のfetchと、tagが未付与のfetch
  • revalidate-tag2-other
    • tag: [”tag2”]のfetchと、tagが未付与のfetch
  • revalidate-tag1-2
    • tag: [”tag1”]のfetchと、tag: [”tag2”]のfetch
  • revalidate-tag12
    • tag: [”tag1”, “tag2”]のfetch

挙動

  • tag1ボタンを押す時
    • ページ内に存在する、fetchのnext.tag: ["tag1"]のキャッシュが破棄される
    • Data Cacheが消えることに連動し、Full Route Cacheも消える
    • ページが再生成される
    • 以下のページがこの挙動をする
      • revalidate-tag1
      • revalidate-tag1-other
        • tag: [”tag1”]が付与されていないfetchは、キャッシュを破棄されない
      • revalidate-tag1-2
        • tag: [”tag2”]のfetchは、キャッシュを破棄されない
      • revalidate-tag12
  • tag2ボタンを押す時
    • ページ内に存在する、fetchのnext.tag: ["tag2"]のキャッシュが破棄される
    • Data Cacheが消えることに連動し、Full Route Cacheも消える
    • ページが再生成される
    • 以下のページがこの挙動をする
      • revalidate-tag2
      • revalidate-tag2-other
        • tag: [”tag2”]が付与されていないfetchは、キャッシュを破棄されない
      • revalidate-tag1-2
        • tag: [”tag1”]のfetchは、キャッシュを破棄されない
      • revalidate-tag12
  • Router Cacheが消えない時もある。。
ちょい戦いメモ

fetchの向き先が全て同じ時、指定していないtagのfetchも再fetchされてしまう問題

コード
const Page = async () => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
    {
      cache: 'force-cache',
      next: {
        tags: ['tag1'],
      },
    },
  )
  const data = await res.json()
  const other = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
    {
      cache: 'force-cache',
    },
  )
  const otherData = await other.json()
  console.log('🚀 ~ revalidate-tag1-other ~')

  return (
    <div>
      <h1>revalidate-tag1-other</h1>
      <pre>tag1: {data.dateTime}</pre>
      <pre>other: {otherData.dateTime}</pre>
    </div>
  )
}

export default Page

挙動

  • revalidateTagを発火
    • tag1ボタン
      • revalidate-tag1-other
        • tag: [”tag1”]が付与されていないfetchも再fetchされている ← ???
      • ...

✅ 1ページ内に定義している、fetchの向き先が同じだった

Request Memoizationが効いて、1リクエストにまとめられているだけだった(それはそう)

向き先変えたら想定通りの動作になりました😄

const Page = async () => {
  const res = await fetch(
    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
    {
      cache: 'force-cache',
      next: {
        tags: ['tag1'],
      },
    },
  )
  const data = await res.json()
  const other = await fetch(
-    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Amsterdam',
+    'https://timeapi.io/api/time/current/zone?timeZone=Europe/Berlin',
    {
      cache: 'force-cache',
    },
  )

revalidatePath, revalidateTagを実行する挙動(トリガー: Server Actions)

revalidatePathを実行する時

Server Actionsのコード

(置き場所は雑です🙏)

src/app/_server-actions/revalidatePath.ts

'use server'
import { revalidatePath } from 'next/cache'

export async function revalidatePathSA(path: string) {
  revalidatePath(path)
  console.log('🚀 ~ SA revalidatePath :', path)
  return { revalidated: true, now: Date.now() }
}

src/app/_server-actions/revalidateTag.ts

'use server'
import { revalidateTag } from 'next/cache'

export async function revalidateTagSA(tag: string) {
  revalidateTag(tag)
  console.log('🚀 ~ SA revalidateTag :', tag)
  return { revalidated: true, now: Date.now() }
}
revalidate-button-saページ
'use client'
import Link from 'next/link'
import { revalidatePathSA } from '../_server-actions/revalidatePath'

const Page = () => {
  console.log('🚀 ~ revalidate-button')

  const revalidatePath = async (path: string) => {
    const data = await revalidatePathSA(`/${path}`)
    if (data.revalidated) {
      alert(`${path}をrevalidateしました`)
    }
  }

  const revalidateTag = async (tag: string) => {
    const data = await revalidatePathSA(tag)
    if (data.revalidated) {
      alert(`${tag}をrevalidateしました`)
    }
  }

  return (
    <div>
      <h1>revalidate-button-sa</h1>

      <p>revalidateボタン</p>
      <div className='flex flex-col items-start gap-4'>
        <div>
          <button
            className='bg-red-500'
            onClick={() => revalidatePath('revalidate-path1')}
          >
            path1
          </button>
          <Link href='/revalidate-path1' prefetch={false}>
            → revalidate-path1
          </Link>
        </div>
        <div>
          <button
            className='bg-red-500'
            onClick={() => revalidatePath('revalidate-path2')}
          >
            path2
          </button>
          <Link href='/revalidate-path2' prefetch={false}>
            → revalidate-path2
          </Link>
        </div>

        <div>
          <button className='bg-red-500' onClick={() => revalidateTag('tag1')}>
            tag1
          </button>
          <Link href='/revalidate-tag1' prefetch={false}>
            → revalidate-tag1
          </Link>
        </div>

        <div>
          <button className='bg-red-500' onClick={() => revalidateTag('tag2')}>
            tag2
          </button>
          <Link href='/revalidate-tag2' prefetch={false}>
            → revalidate-tag2
          </Link>
        </div>

        <Link href='/revalidate-tag1-other' prefetch={false}>
          → revalidate-tag1-other
        </Link>
        <Link href='/revalidate-tag2-other' prefetch={false}>
          → revalidate-tag2-other
        </Link>

        <Link href='/revalidate-tag1-2' prefetch={false}>
          → revalidate-tag1-2
        </Link>

        <div>
          <button
            className='bg-red-500'
            onClick={async () => {
              await revalidateTag('tag1')
              await revalidateTag('tag2')
            }}
          >
            revalidate-tag-12
          </button>
          <Link href='/revalidate-tag12' prefetch={false}>
            → revalidate-tag12
          </Link>
        </div>
      </div>
    </div>
  )
}

export default Page

挙動

  • 特定のページが以下の動作をする
    • ページ内に存在する、fetch: cache: “force-cache”が再度発火する
    • Data Cacheが消えることで、Full Route Cacheが消える
    • ページが再レンダリングされる
  • Router Cacheが確実に消える!

revalidateTagを実行する時

(ページはRoute Handlersをトリガーとしていた時と同じです)

挙動

  • revalidateTagを発火
    • tag1ボタン → 以下が再レンダリング
      • revalidate-tag1
      • revalidate-tag1-other
        • tag: [”tag1”]が付与されていないfetchは、fetchされない
      • revalidate-tag1-2
        • tag: [”tag2”]のfetchは、fetchされない
      • revalidate-tag12
    • tag2ボタン
      • revalidate-tag2
      • revalidate-tag2-other
        • tag: [”tag2”]が付与されていないfetchは、fetchされない
      • revalidate-tag1-2
        • tag: [”tag1”]のfetchは、fetchされない
      • revalidate-tag12
  • Router Cacheが確実に消える!


Request Memoization

同じページの同一レンダリングで、同じリクエストをする時に、1リクエストにまとめるメモ化
レンダリングが完了すると、メモリはリセットされる
このメモ化はGETメソッドのみに適用される

オプトイン・オプトアウト

オプトイン

  • デフォルトで有効

オプトアウト

  • オプトインを推奨している
  • 以下のコードでオプトアウト出来る
const { signal } = new AbortController()
fetch(url, { signal })

挙動

ページ内に、同じAPIへ向けたリクエストを2回行う

→ Route Handlersログが1回のみなので効いている

APIのコード

src/app/api/health/route.ts

export const GET = () => {
  console.log('GET /api/health')
  console.log('🚀 ~ GET ~ /api/health')
  return Response.json({ data: 'healthy' })
}
ページのコード

src/app/request-memoization/page.tsx

const Page = async () => {
  console.log('🚀 ~ request-memoization ~')

  return (
    <div>
      <h1>request-memoization</h1>
      <FetchComponent1 />
      <FetchComponent2 />
    </div>
  )
}

const FetchComponent1 = async () => {
  const res = await fetch('http://localhost:3000/api/health', {
    cache: 'no-store',
  })
  const data = await res.json()

  return (
    <div className='bg-red-800'>
      <h1>FetchComponent1</h1>
      <pre>message: {data.data}</pre>
    </div>
  )
}

const FetchComponent2 = async () => {
  const res = await fetch('http://localhost:3000/api/health', {
    cache: 'no-store',
  })
  const res2 = await fetch('http://localhost:3000/api/health', {
    cache: 'no-store',
  })
  const data = await res.json()
  const data2 = await res2.json()

  return (
    <div className='bg-blue-800'>
      <h1>FetchComponent2</h1>
      <pre>message: {data.data}</pre>
      <pre>message: {data2.data}</pre>
    </div>
  )
}

export default Page

別のAPIだと2回ログが出る

修正部分

...

const FetchComponent1 = async () => {
-  const res = await fetch('http://localhost:3000/api/health', {
+  const res = await fetch('http://localhost:3000/api/health2', {
    cache: 'no-store',
  })
...
}

Router Cache

訪れたページをキャッシュする
キャッシュはページ遷移と<Link prefetch>router.prefetch()をトリガーに行われる
リロードしたらキャッシュは破棄される
静的、動的のページ関係なくキャッシュする
キャッシュが効いている間は、Server Componentsが再レンダリングされず、fetchのcache: "no-store"が有効でも再実行されない

  1. <Link prefetch={null | true | false}>
    • null (デフォルト)
      • <Link>が画面内に表示されているか、<Link>をホバーしたタイミングでprefetchする
      • 静的ページは、Full Route Cacheと同じものをキャッシュする
      • 動的ページの場合、loading.jsファイルを持つ最も近いルートセグメントまでprefetchする。ローディングファイルがない場合は、データの取りすぎを避けるために完全なツリーをフェッチしない。
    • true
      • <Link>が画面内に表示されているか、<Link>をホバーしたタイミングでprefetchする
      • 動的・静的ページのFull Route Cacheと同じものをキャッシュする
      • 動的ページはstaleTimes.staticの時間キャッシュを保持する
    • false
      • データをプリフェッチしない(ビューに<Link>が表示、<Link>をホバーされても)
  2. router.prefetch()
    • ルートを手動でプリフェッチすることが出来る
    • React Server Components PayLoadをキャッシュする
  3. staleTimes
    • dynamic
      • 動的なページのキャッシュ時間
      • デフォルト: 0s
    • static
      • 静的なページのキャッシュ時間
      • デフォルト: 5m

オプトイン・オプトアウト

デフォルト

  • 動的なページは無効。静的なページは5分間キャッシュする

オプトイン

  • staleTimesを1s以上に設定する

オプトアウト

  • staleTimesを0にする
  • 以下のいずれかを発火すると、全てのページのRouter Cacheを破棄する
    • router.refresh()
    • revalidatePath()(引数に無いページでも)
    • revalidateTag()(引数に無いTagのページも)

prefetch={null}(デフォルト)

<Link href={...}>
...
</Link>

静的・動的オプトアウト + prefetch={null}

staleTimes: {
  dynamic: 0,
  static: 0,
},

挙動

  • 動的ページ
    • <Link>が画面内に表示されているか、<Link>をホバーしたタイミングでprefetchされる
    • キャッシュを保持せず訪れる度にページを再実行する

静的・動的オプトイン + prefetch={null}

staleTimes: {
  dynamic: 3, // 3s
  static: 3, // 3s
},

挙動

  • 動的ページ
    • <Link>が画面内に表示されているか、<Link>をホバーしたタイミングでprefetchされる
    • 訪れたページのRSC PayloadをstaleTimes.dynamicで指定した期間キャッシュする

prefetch={true}の時

<Link href={...} prefetch={true}>
...
</Link>

静的・動的オプトアウト + prefetch={true}

挙動

  • 動的ページ
    • <Link>が画面内に表示されているか、<Link>をホバーしたタイミングでprefetchされる
      • staleTimes.staticが0なので、ホバーする度にprefetchが走る
    • キャッシュを保持せず訪れる度にページを再実行する

静的・動的オプトイン + prefetch={true}

staleTimes: {
  dynamic: 1,
  static: 5,
},

挙動

  • 動的ページ
    • <Link>が画面内に表示されているか、<Link>をホバーしたタイミングでprefetchされる
    • 訪れたか、prefetchされたページのRSC PayloadをstaleTimes.staticで指定した期間キャッシュする
      • prefetch={true}の場合、動的なページはstaleTimes.staticが適用される

prefetch={false}の時

<Link href={...} prefetch={false}>
...
</Link>

静的・動的オプトアウト + prefetch={false}

挙動

  • 動的ページ
    • <Link>が画面内に表示されていても、<Link>をホバーしてもprefetchされない
    • キャッシュを保持せず訪れる度にページを再実行する

静的・動的オプトイン + prefetch={false}

挙動

  • 動的ページ
    • <Link>が画面内に表示されていても、<Link>をホバーしてもprefetchされない
    • 訪れたページのRSC PayloadをstaleTimes.dynamicで指定した期間キャッシュする

router.refresh()を使ったオプトアウト

staleTimes: {
  dynamic: 5,
  static: 5,
},

router-cache-buttonのコード

import Link from 'next/link'
import { useRouter } from 'next/navigation'

const Page = () => {
  const router = useRouter()
  console.log('🚀 ~ router-cache-button:')
  return (
    <div>
      <h1>router-cache-button</h1>
      <button
        className='bg-red-500'
        onClick={() => {
          router.refresh()
          alert('router.refresh()しました')
        }}
      >
        router.refresh()
      </button>

      <div className='flex flex-col items-start gap-4'>
        <Link href='/router-cache-dynamic' prefetch={false}>
          → 動的: router-cache-dynamic
        </Link>
        <Link href='/router-cache-static' prefetch={false}>
          → 静的: router-cache-static
        </Link>
      </div>
    </div>
  )
}

export default Page

挙動

  • 動的なページ・静的なページのRouter Cacheを破棄する
  • 動的なページは、訪れた時にページが再実行される
  • 静的なページは、訪れた時にビルド時に生成されたRSC Payloadが再取得される

revalidatePath, revalidateTagを使ったオプトアウト

挙動

  • 動的なページ・静的なページのRouter Cacheを破棄する
  • 動的なページは、訪れた時にページが再実行される
  • 静的なページは、訪れた時にビルド時に生成されたRSC Payloadが再取得される

動作については、Data Cacheの以下を参照


Full RouteCache

オプトイン・オプトアウト

オプトイン

  • デフォルト
  • 静的なページであればON

オプトアウト

  • 動的なページにする

ビルドの挙動

動的ページ(Dynamic APIを使う)

以下を使うページは動的なページとなる

  • cookies
  • headers
  • searchParams

各ページのコード
// full-route-cookieページ
import { cookies } from 'next/headers'

const Page = async () => {
  const cookieString = (await cookies()).toString()
  ...
}

// full-route-headerページ
import { headers } from 'next/headers'

const Page = async () => {
  const headerList = await headers()
  ...
}

// full-route-paramsページ
const Page = async ({
  searchParams,
}: {
  searchParams: Promise<{ id: string }>
}) => {
  const id = (await searchParams).id
  ...
}

動的ページ(const dynamic = 'force-dynamic'const revalidate = 0を定義する)

以下の定義をするページは動的なページとなる

  • const dynamic = 'force-dynamic'
  • const revalidate = 0

各ページのコード
// full-route-force-dynamicページ
export const dynamic = 'force-dynamic'
const Page = async () => {
  return (
    <div>
      <h1>full-route-force-dynamic</h1>
    </div>
  )
}

export default Page
// full-route-revalidate-0ページ
export const revalidate = 0
const Page = async () => {
  return (
    <div>
      <h1>full-route-revalidate-0</h1>
    </div>
  )
}

export default Page

動的ページ(Data Cacheをオプトアウトする)

Data Cacheのオプトアウトを参照

静的ページ

上記以外のページは静的ページとなる

参考

https://nextjs.org/docs/app/building-your-application/caching

Discussion