# Next.js のキャッシュ
Next.js App Router では、レンダリング結果とデータ取得を複数のキャッシュ層で最適化する。ここでは 4 つのキャッシュ機構と、fetch オプション・Route Segment Config・revalidate の関係を整理する。
# 4 つのキャッシュ機構
| 機構 | 対象 | 場所 | 目的 | 有効期間 |
|---|---|---|---|---|
| Request Memoization | 関数の戻り値 | Server | 同一リクエスト内で重複 fetch を抑止 | リクエストライフサイクル |
| Data Cache | データ | Server | リクエスト・デプロイをまたいでデータを保持 | 永続(revalidate 可能) |
| Full Route Cache | HTML / RSC Payload | Server | レンダリングコスト削減 | 永続(revalidate 可能) |
| Router Cache | RSC Payload | Client | ナビゲーション時のサーバーリクエスト削減 | セッション or 時間ベース |
デフォルトでは「できるだけキャッシュする」方針。ルートが静的レンダリングされ、キャッシュ可能なデータは Data Cache に載る。動的 API を使うとそのルートは動的レンダリングになり、Full Route Cache には載らない。
# レンダリング戦略
- Static Rendering: ビルド時(または revalidate 後)にレンダリング。結果は Full Route Cache に載る。
- Dynamic Rendering: リクエストごとにレンダリング。
cookies/headers/searchParams/unstable_noStore/fetchの{ cache: 'no-store' }などを使うと動的になる。
動的ルートでも Data Cache は利用できる(個別の fetch がキャッシュされていれば)。
# Request Memoization
同じ URL・オプションの fetch(GET/HEAD)は、1 回の React のレンダーパス内で 1 回だけ実行され、他はメモ化された結果が返る。Layout / Page / 複数コンポーネントで同じデータを何度も読んでも、ネットワークリクエストは 1 回で済む。
- 有効範囲: サーバーリクエストのレンダーが終わるまで。
- オプトアウト:
fetchにAbortControllerのsignalを渡すとメモ化の対象外(通常はオプトアウトしない想定)。
fetch 以外(DB クライアント・CMS など)で同じ「1 リクエスト 1 実行」にしたい場合は、React の cache() でラップする。
// utils/get-item.ts
import { cache } from "react";
import db from "@/lib/db";
export const getItem = cache(async (id: string) => {
const item = await db.item.findUnique({ where: { id } });
return item;
});
# Data Cache
サーバー側の fetch の結果を、リクエストやデプロイをまたいで保持する。Next.js は fetch に cache と next.revalidate を拡張している。
- デフォルト:
fetchは Data Cache に自動では載らない。- 静的ルートでは「キャッシュ可能」と判断された fetch が Data Cache に入り、レンダー結果は Full Route Cache に載る。
- 動的ルートでは毎回取得(
cache: 'no-store'相当)。
- キャッシュする場合:
cache: 'force-cache'を付ける。 - キャッシュしない場合:
cache: 'no-store'を付ける(動的レンダリングのトリガーにもなる)。
開発モードでは HMR 用に fetch 結果が再利用され、ハードリフレッシュ時はキャッシュオプションは無視される。
# 時間ベースの revalidate
一定時間ごとに再検証するには next.revalidate を使う(秒単位)。stale-while-revalidate 的な動きになる。
// 最大 1 時間キャッシュ
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 3600 },
});
# オンデマンド revalidate
- タグ:
next.tagsでタグを付け、revalidateTag(tag)でそのタグのエントリだけ無効化。 - パス:
revalidatePath(path)で指定パス以下の Data Cache と Full Route Cache を無効化。
// fetch にタグを付ける
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
// Server Action や Route Handler から
import { revalidateTag, revalidatePath } from "next/cache";
revalidateTag("posts");
revalidatePath("/blog");
revalidateTag / revalidatePath は Server Action 内で呼ぶと Router Cache も即時無効化される。Route Handler だけだと Data Cache は無効化されるが、Router Cache は即時には更新されない(ハードリフレッシュや自動無効化時間まで待つ)。
# Full Route Cache
静的レンダリングされたルートの HTML と RSC Payload をサーバー側でキャッシュする。
- Data Cache の revalidate やオプトアウトは、そのルートの Full Route Cache にも影響する(データが変わればレンダー結果も再生成)。
- 逆に、Full Route Cache をオプトアウトしても Data Cache はそのまま使える(動的レンダリング + 一部だけキャッシュ、という組み合わせが可能)。
- デプロイ時に Full Route Cache はクリアされる(Data Cache はデプロイをまたいで永続する)。
オプトアウトするには、例えば次のいずれかを使う。
- 動的 API(
cookies,headers,searchParamsなど)を使う。 export const dynamic = 'force-dynamic'またはrevalidate = 0をセグメントに設定する。- そのルート内のいずれかの
fetchをcache: 'no-store'にする。
# Router Cache(クライアント)
クライアント側のメモリに、ルートセグメント単位で RSC Payload を保持する。同一セッション内のナビゲーションや prefetch で再利用され、バック/フォワードや <Link> の prefetch が速くなる。
- 有効期間: ナビゲーション中は保持。フルリロードでクリア。prefetch の種類や静的/動的によって、一定時間で自動無効化される(例: 静的 5 分など)。
- 無効化: Server Action 内の
revalidatePath/revalidateTag、またはcookies.set/cookies.delete、あるいはクライアントのrouter.refresh()。
router.refresh() は Router Cache をクリアして現在ルートを再取得するだけで、Data Cache / Full Route Cache は変更しない。
# Route Segment Config
Page / Layout / Route Handler で export const してキャッシュや動的挙動を上書きする。
# dynamic
'auto'(デフォルト): 動的 API がなければ可能な限り静的。'force-dynamic': 常に動的レンダリング。セグメント内の fetch は実質cache: 'no-store',revalidate: 0扱い。'force-static':cookies/headers等を空にしてでも静的にする。'error': 動的 API やキャッシュなしデータがあるとエラー(完全に静的であることを強制)。
# revalidate
false: デフォルト。キャッシュはヒューリスティックに決定。0: そのセグメントは常に動的レンダリング(fetch のデフォルトが no-store 相当になる)。number: 秒単位のデフォルト revalidate。ルート全体では、レイアウト〜ページで一番短い revalidate が使われる。
revalidate は静的に解析できる値である必要がある(600 は OK、60 * 10 は NG)。
# fetchCache
セグメント内の全 fetch のデフォルトを上書きする(必要なときだけ使う)。
'default-no-store': オプション未指定の fetch をno-store扱い(ルートを動的にしたいとき)。'force-no-store': すべての fetch を強制的にno-store。'default-cache'/'force-cache'/'only-cache': キャッシュを強めたい場合。
# API とキャッシュの関係(簡易)
| API | Router Cache | Full Route Cache | Data Cache |
|---|---|---|---|
<Link prefetch> | 保存 | - | - |
router.refresh() | 無効化 | - | - |
fetch(オプションなし) | - | 静的時は保存 | 静的時は保存 |
fetch cache: 'force-cache' | - | - | 保存 |
fetch cache: 'no-store' | - | オプトアウト | オプトアウト |
fetch next.revalidate | - | 再検証 | 再検証 |
fetch next.tags + revalidateTag | Server Action で無効化 | 無効化 | 無効化 |
revalidatePath | Server Action で無効化 | 無効化 | 無効化 |
dynamic = 'force-dynamic' | - | オプトアウト | オプトアウト |
revalidate = 0 | - | オプトアウト | オプトアウト |
# 実践例
# ブログ一覧を 1 時間キャッシュ
// app/blog/page.tsx
export default async function BlogPage() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
})
const posts = await res.json()
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
)
}
# CMS 更新時にタグで revalidate
// app/blog/page.tsx
const res = await fetch("https://cms.example.com/posts", {
next: { tags: ["cms-posts"] },
});
// app/actions.ts
("use server");
import { revalidateTag } from "next/cache";
export async function onCmsWebhook() {
revalidateTag("cms-posts");
}
# ルート全体を常に動的にする
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'
export default async function DashboardPage() {
const data = await fetch('https://api.example.com/me', {
cache: 'no-store',
}).then((r) => r.json())
return <div>{data.name}</div>
}
# キャッシュと動的のハイブリッド
静的レンダリングのルートで、一部だけリクエストごとに取得したい場合は、その fetch に cache: 'no-store' を付ける。それ以外の fetch は force-cache や next.revalidate のままにできる。