Next.js 16 の Cache Components と PPR - キャッシュを「明示的」に組み直す

Next.js 16 の Cache Components と PPR - キャッシュを「明示的」に組み直す

作成日:
更新日:

Next.js の App Router は強力ですが、「いつ・何がキャッシュされているのか分かりにくい」というのが長年の不満でした。fetch が暗黙にキャッシュされ、意図せず古いデータが出たり、逆にキャッシュが効かなかったり——。

Next.js 16 の Cache Components は、この前提をひっくり返します。暗黙キャッシュをやめ、「キャッシュするものだけ明示する」opt-in 方式へ。あわせて Partial Prerendering(PPR)で「静的な部分と動的な部分を1ページ内で混ぜる」のが標準になります。このブログ(Next.js 構成)にも直結する話を、公式ドキュメントをもとに整理します。

発想の転換: 暗黙キャッシュをやめる

next.configcacheComponents: true を有効にすると、PPR と use cache ディレクティブが一緒に有効になります。

next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  cacheComponents: true,
}
 
export default nextConfig

最大の変化はこれです。Cache Components 有効時、暗黙的なキャッシュは廃止され、ページ・レイアウト・ルートハンドラ内の動的コードはデフォルトで毎リクエスト実行されます。キャッシュは「明示的に有効化するもの(opt-in)」になりました。「気づかぬうちにキャッシュされていた」が無くなる代わりに、「キャッシュしたい場所は自分で宣言する」のが基本になります。

NOTE

旧来の experimental.dynamicIOcacheComponents にリネームされ、experimental.useCache も統合されました。experimental.ppr フラグやルートごとの export const experimental_ppr も削除され、すべて cacheComponents に集約されています。

use cache: キャッシュしたい場所を宣言する

キャッシュは 'use cache' ディレクティブで宣言します。粒度は3レベルです。

  • ファイルレベル: ファイル先頭に書くと、その全 export 関数が対象(すべて async 必須)
  • コンポーネントレベル: コンポーネント関数の先頭に書く
  • 関数レベル: 任意の async 関数(fetch・DB クエリ・重い計算など)の先頭に書く
関数レベルのキャッシュ
import { cacheLife, cacheTag } from 'next/cache'
 
export async function getPosts() {
  'use cache'
  cacheLife('hours')   // 有効期間プロファイル
  cacheTag('posts')    // タグ付けしてオンデマンド無効化
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

キャッシュキーは「ビルド ID + 関数の場所・シグネチャのハッシュ + シリアライズ可能な引数」から自動生成されます。外側スコープの変数も引数としてキーに含まれます。

WARNING

キャッシュの内側では cookies()headers()searchParams といったリクエスト時 API を直接使えません。使いたい場合はキャッシュの外で読み、引数として渡すのが定石です(その値がキャッシュキーに加わる)。引数・戻り値はシリアライズ可能である必要があり、クラスインスタンスや関数などは渡せません。ここを外すと、ビルドが約50秒でタイムアウトするなどハマりやすいので注意です。

PPR: 静的シェル+動的ストリーミング

Cache Components を有効にすると、Partial Prerendering(PPR)がデフォルト挙動になります。PPR は、静的な HTML「シェル」を即座に返し、動的な部分を同じレスポンス内でストリーミングする方式です。ページ単位ではなくコンポーネント単位で静的・動的を混ぜられます。

考え方はシンプルで、「<Suspense> の外側は静的、内側は動的」。

静的と動的を1ページで混ぜる
import { Suspense } from 'react'
 
export default function BlogPage() {
  return (
    <>
      <header>{/* 決定的: 自動で静的シェルに入る */}</header>
      <BlogPosts />               {/* use cache: 静的シェルに含む */}
      <Suspense fallback={<p>Loading...</p>}>
        <UserPreferences />       {/* cookie 依存: リクエスト時にストリーム */}
      </Suspense>
    </>
  )
}
 
async function BlogPosts() {
  'use cache'
  cacheLife('hours')
  const res = await fetch('https://api.example.com/posts')
  return /* ... */
}
 
async function UserPreferences() {
  const theme = (await cookies()).get('theme')?.value ?? 'light'
  return /* ... */
}

ビルド時、Next.js はツリーをたどり、use cache の結果と決定的な処理を静的シェルに含め、<Suspense> の中身はリクエスト時にストリームします。

NOTE

注意点として、未キャッシュのデータにアクセスするコンポーネントを <Suspense> でも use cache でも包まないと、Uncached data was accessed outside of <Suspense> エラーになります。v16 は「どちらに倒すか」を明示させる設計です。逆に、同期処理だけのコンポーネントは <Suspense> で包んでも静的に解決されます(<Suspense> が動的化のスイッチではない)。

設計の三分類と、リバリデーションの使い分け

実務では、コンポーネントを次の3つに振り分けるのが基本です。

種類扱い
静的(決定的のみ)ヘッダー・ナビ何もしない(自動で静的シェル)
全ユーザー共通の動的記事一覧・カタログuse cache + cacheLife + cacheTag
リクエスト固有cookie 依存のユーザー設定<Suspense> でストリーム

無効化(リバリデーション)の API も整理されました。

  • revalidateTag(tag, profile): stale-while-revalidate。多少の遅延が許せるもの(ブログ・カタログ)。v16 では第2引数のプロファイルが必須に(例: revalidateTag('posts', 'max')
  • updateTag(tag)(Server Actions 専用・新設): read-your-writes。フォーム送信後など即時反映が要る場面
  • refresh()(Server Actions 専用・新設): 未キャッシュデータだけを更新(キャッシュには触れない)。通知カウントなど

v15 から v16 への移行で詰まりやすい点

移行は codemod で大半を自動化できます。

アップグレード
npx @next/codemod@canary upgrade latest

主な注意点です。

  • 要件: Node.js 20.9 以上、TypeScript 5.1 以上、モダンブラウザ(Chrome/Edge/Firefox 111+、Safari 16.4+)
  • Async Request API: cookies() / headers() / draftMode()params / searchParams必ず await(v15 の同期互換は廃止)
  • Turbopack がデフォルト: --turbopack フラグ不要。カスタム webpack 設定があると next build が失敗するので --webpack で明示オプトアウト
  • middleware.tsproxy.ts(関数名も proxy。Node ランタイム固定で edge 非対応)
  • PPR の挙動が v15 canary と異なる: v15 canary で PPR を使っているなら、いったんそこに留まり、公式の「Migrating to Cache Components」ガイドの移行パターンに従う。experimental.ppr: true はそのままでは v16 で機能しない
  • revalidateTag は第2引数必須に。unstable_cacheLife / unstable_cacheTag は安定化(プレフィックス不要)

NOTE

画像まわりのデフォルトも変わっています(Cache Components とは別だが移行注意)。images.minimumCacheTTL が60秒→4時間、images.qualities が全許可→[75] のみ、images.domains が非推奨(remotePatterns へ)など。アップグレード後に画像の挙動が変わって見えたらここを確認してください。

まとめ

  • Next.js 16 の Cache Components(cacheComponents: trueは、暗黙キャッシュをやめ「キャッシュするものだけ use cache で明示する」opt-in 方式
  • use cache はファイル/コンポーネント/関数の3粒度。内側で cookies() 等は使えず、外で読んで引数で渡す
  • PPR は静的シェル+動的ストリーミング。「<Suspense> の外は静的、内は動的」。未キャッシュデータは Suspense か use cache で必ず包む
  • 無効化は revalidateTag(遅延可・プロファイル必須)/ updateTag(即時・Server Actions)/ refresh を使い分け
  • 移行は codemod。Node 20.9+、await 必須、Turbopack 既定、middlewareproxy、PPR は v15 canary と非互換に注意

「キャッシュは賢く自動で」から「キャッシュは明示的に宣言する」へ。最初は手間に見えますが、「どこがなぜキャッシュされるか」がコードから読めるようになるのが Cache Components の狙いです。

参考リンク