Next.js v15.0.3における4つのキャッシュ挙動調査まとめ
⚠️注意点
- 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を実行しキャッシュする
- ページは静的なページとなる(他に動的なページとなるコードが無ければ)
- 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
const fetchCache
と、const dynamic
と、const revalidate
の組み合わせの挙動
dynamic APIsと、-
dynamic APIs VS
cache: “force-cache”
→ 動的なページとなる
→ fetchはbuild時に行われず、初回リクエスト時のfetchをキャッシュする
→ cookie、headers、searchParamsは表示される -
dynamic APIs VS
const dynamic = 'force-static'
→ 静的なページとなる
→ fetchはbuild時のみ行われキャッシュする
→ cookie、headers、searchParamsは表示されない -
dynamic APIs VS
const dynamic = 'force-static'
VScache: “no-store”
→ 静的なページとなる
→ fetchはbuild時のみ行われキャッシュする
→ cookie、headers、searchParamsは表示されない -
const fetchCache = 'default-no-store'
→ 動的なページになる
→ fetchはリクエストの度行われ、キャッシュはされない -
const fetchCache = 'default-no-store'
VScache: “force-cache”
→ 静的なページになる
→ fetchはbuild時のみ行われキャッシュする -
const dynamic = 'force-dynamic'
VScache: “force-cache”
→ 動的なページになる
→ fetchはbuild時に行われず、初回リクエスト時のfetchをキャッシュする -
const dynamic = 'force-static'
VScache: “no-store”
→ 静的なページになる
→ fetchはbuild時のみ行われキャッシュする -
const revalidate = 0
→ 動的なページになる
→ fetchはリクエストの度行われ、キャッシュはされない -
const revalidate = 0
VScache: “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
- ページ内に存在する、fetchの
- 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
- ページ内に存在する、fetchの
- 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されている ← ???
- ...
- revalidate-tag1-other
- tag1ボタン
✅ 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
- tag1ボタン → 以下が再レンダリング
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"
が有効でも再実行されない
-
<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>
をホバーされても)
- データをプリフェッチしない(ビューに
- null (デフォルト)
-
router.prefetch()
- ルートを手動でプリフェッチすることが出来る
- React Server Components PayLoadをキャッシュする
-
staleTimes
- dynamic
- 動的なページのキャッシュ時間
- デフォルト: 0s
- static
- 静的なページのキャッシュ時間
- デフォルト: 5m
- dynamic
オプトイン・オプトアウト
デフォルト
- 動的なページは無効。静的なページは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の以下を参照
-
revalidatePath, revalidateTagを実行する挙動(トリガー: Route Handlers)
- Route Handlersがトリガーだと、Router Cacheが破棄されない時もある
- revalidatePath, revalidateTagを実行する挙動(トリガー: Server Actions)
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をオプトアウトする)
静的ページ
上記以外のページは静的ページとなる
参考
Discussion