# 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 回で済む。

  • 有効範囲: サーバーリクエストのレンダーが終わるまで。
  • オプトアウト: fetchAbortControllersignal を渡すとメモ化の対象外(通常はオプトアウトしない想定)。

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 は fetchcachenext.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 / revalidatePathServer 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 をセグメントに設定する。
  • そのルート内のいずれかの fetchcache: '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-cachenext.revalidate のままにできる。

# 参考

2026-01-30

同じタグを持つ記事をピックアップしました。