TL;DR
- Next.js Cache Components は PPR(Partial Prerendering)の延長線 にある宣言的キャッシュモデル。
'use cache'directive をキャッシュ単位とし、cacheLife/cacheTagで TTL と invalidation を declarative に書ける(公式: use cache directive / Vercel: Partial Prerendering announcement) - 実務判定は Yes / No / Defer の3分岐。判断軸は「App Router 100% か」「PPR を本番 or stage で動かしたか」「キャッシュヒット率を観測できるか」の3問。最初の2問で過半が決まる(経験則)
- ISR の
revalidateよりも 粒度がコンポーネント / 関数単位 まで下がるため、運用負債(古いキャッシュフラグの散乱)が減る。LCP にも直接効く(web.dev: LCP) - 段階移行は 「静的ページ → 動的ページの一部 → 全面適用」の4ステップ。本記事は末尾に12項目の移行チェックリスト + PR テンプレ + CI ガード GitHub Actions snippet を配布する
- ただし
next devの Turbopack と相性で stale を引きやすい、'use cache'内で cookies/headers が読めない、旧fetchキャッシュとの衝突など 5つの罠 があるため §5 を必ず通読してほしい(React 公式: Server Components RFC も併読推奨)
はじめに
こんにちは、みねです。
「PPR を本番投入していたが、Next.js 16 系で Cache Components が stable 化したと聞いた。'use cache' って結局何なのか。今 ISR/PPR で動いているコードをどう移行するか」── Tech Lead から最近よく受ける相談です。
結論から言うと、Cache Components は PPR の置き換えではなく延長線です。PPR が「静的シェルに動的穴を空ける」レイアウト戦略だったのに対し、Cache Components は「キャッシュ境界を宣言的に書く」プログラミングモデルです。両者は併用が前提で、PPR は引き続き有効です。
ただし、移行を急ぐ必要はチームによって違います。PPR 未経験のチームがいきなり Cache Components に飛ぶと、キャッシュ境界の設計勘所がないため事故りやすい。逆に PPR を1年運用していたチームには、'use cache' は「待っていた API」です。
この記事のゴールは、移行可否を1日で判定できる材料を、判断軸 / 比較表 / 段階移行手順 / 5つの罠 / チェックリスト / PR テンプレまで含めて整理することです。前提として Next.js 16.x 系(2026-05時点)を想定しています。
関連記事として、API 層の選定なら tRPC を採用すべきか 型安全API設計の現実解、フロント性能の優先順位は フロント性能改善の正しい順序 を参照してください。
§1. Next.js Cache Components とは何か(PPR との違い)
1-1. PPR の復習 ── 静的シェル + 動的穴
PPR(Partial Prerendering)は、1つのページを「ビルド時に静的レンダリングするシェル」と「リクエスト時に動的レンダリングする穴(Suspense 境界)」に分割するレンダリング戦略です(公式: Partial Prerendering)。
// Next.js 15 系 PPR の典型コード
import { Suspense } from 'react'
export default function Page() {
return (
<>
<StaticHeader />
<Suspense fallback={<Skeleton />}>
<DynamicCart /> {/* リクエスト時に評価 */}
</Suspense>
</>
)
}
PPR の利点は TTFB(Time To First Byte)を静的並みに維持しつつ、動的部分だけ後から流す こと。LCP に効きます。
1-2. Cache Components の定義 ── 「'use cache' を境界とするキャッシュモデル」
Cache Components は、'use cache' directive を境界としてキャッシュを宣言する仕組みです。ファイル単位 / コンポーネント単位 / 関数単位 の3粒度で書けます。
// ファイル単位
'use cache'
export default async function Page() {
const products = await fetchProducts()
return <ProductList items={products} />
}
// 関数単位(v15 までの旧形式: unstable_cache。v16 では `'use cache'` directive を推奨)
async function getProducts() {
'use cache'
return await fetchProducts()
}
旧 fetch({ next: { revalidate: 60 } }) のような「fetch ごとのキャッシュ設定」ではなく、「この関数はキャッシュ可能」を関数側で宣言する のが本質的な違いです。
1-3. 旧 fetch キャッシュとの違い(比較表)
| 項目 | 旧 fetch キャッシュ(〜Next.js 14) | Cache Components(Next.js 16〜) |
|---|---|---|
| キャッシュ単位 | リクエスト(fetch 呼び出し)単位 | 関数 / コンポーネント / ファイル単位 |
| 設定箇所 | fetch のオプション引数 | 'use cache' directive + cacheLife / cacheTag |
| TTL の表現 | next: { revalidate: N } | cacheLife('hours') などプリセット名 |
| Invalidation | revalidatePath / revalidateTag | 同上(API は継承) |
| 副作用との分離 | 緩い(fetch 内に cookies 読み書き混在しがち) | 厳格(cookies/headers 禁止) |
| デフォルト | Next.js 13 系の「常時キャッシュ」から 14 系で auto no cache(dev は毎回取得 / 静的 prerender 時はビルド時1回)に変更(公式値) | デフォルトは opt-in('use cache' 明示時のみ) |
ポイントは 「キャッシュは宣言的に opt-in」 に変わったこと。Next.js 14 系で「デフォルト no-store」になった反省を引き継いでいます。
§2. 移行可否を1日で判定する3つの問い
flowchart TD
Q1{App Router 100%?} -->|No| DEFER1[Defer: Pages Router 残存]
Q1 -->|Yes| Q2{PPR 運用経験?}
Q2 -->|No| DEFER2[Defer: PPR から始める]
Q2 -->|Yes| Q3{キャッシュ観測あり?}
Q3 -->|No| NO[No: 観測整備が先]
Q3 -->|Yes| YES[Yes: 移行 PoC 開始]
2-1. 問1「App Router 100% か?」── Pages Router 残存の罠
Cache Components は App Router 専用 です。Pages Router と混在しているプロジェクトでは、Pages Router 側のキャッシュ動作(getStaticProps の revalidate)が別物として残るため、チームの認知負荷が一気に倍増 します。
判定: Pages Router が 1 ページでも残っていれば、まず移行を完了させる。Cache Components は次のステップ。
2-2. 問2「PPR を本番 or stage で動かしたか?」
PPR を運用したことがないチームが Cache Components に直行するのは、キャッシュ境界の設計勘所がないまま導入することになり危険 です(経験則)。
PPR 未経験 = 「どこまで静的にできるか」「どこを Suspense で切るか」の感覚がない。Cache Components はその上位レイヤーなので、PPR を最低でも stage で1ヶ月運用してから移行を判断するのが現実的です。
2-3. 問3「キャッシュヒット率を観測できるか?」
Cache Components の効果は キャッシュヒット率 で測るしかありません。Vercel Analytics か自前で x-vercel-cache ヘッダー / Edge Runtime の cache hit ログを集計できないと、「移行したけど効果が出ているか分からない」状態になります。
判定: 観測整備がない場合は 移行前に観測を入れる。これは フロント性能改善の正しい順序 でも触れた「測れないものは改善できない」原則そのままです。
2-4. Yes / No / Defer の3分岐まとめ
| 判定 | 条件 | 次アクション |
|---|---|---|
| Yes | App Router 100% + PPR 運用済み + 観測あり | 1日 PoC で feature flag 導入 |
| Defer | App Router 100% + PPR 未経験 | PPR を stage で1ヶ月運用してから再判定 |
| Defer | Pages Router 残存 | Router 統一を完了させる |
| No | 観測なし | Vercel Analytics or 自前観測導入が先 |
§3. 'use cache' / cacheLife / cacheTag の使い分け
3-1. 3粒度のコード例
// 粒度1: ファイル全体(最も粗い)
'use cache'
export default async function ProductsPage() {
const products = await db.products.findMany()
return <ProductList items={products} />
}
// 粒度2: 関数単位(推奨。最も使い分けしやすい)
async function getProducts() {
'use cache'
const products = await db.products.findMany()
return products
}
// 粒度3: コンポーネント単位(部分的キャッシュ)
async function ProductGrid() {
'use cache'
const products = await getProducts()
return <Grid items={products} />
}
実務で最も使うのは粒度2(関数単位) です。理由は、コンポーネント側を pure に保ちながら、データ取得層だけキャッシュ可能になるため。テストもしやすい。
3-2. cacheLife プリセット早見表(公式値)
import { cacheLife } from 'next/cache'
async function getProducts() {
'use cache'
cacheLife('hours') // プリセット名で指定
return await db.products.findMany()
}
| プリセット | stale | revalidate | expire |
|---|---|---|---|
seconds | 30 秒 | 1 秒 | 1 分 |
minutes | 5 分 | 1 分 | 1 時間 |
hours | 5 分 | 1 時間 | 1 日 |
days | 5 分 | 1 日 | 1 週間 |
weeks | 5 分 | 1 週間 | 1 ヶ月 |
max | 5 分 | 1 ヶ月 | 1 年 |
上記は公式値(Next.js docs)。プロジェクト固有のプリセットは
next.config.tsで定義可能。
判断基準: 商品マスタのような「数時間更新で OK」は hours、ブログ記事は days、ヘッダーナビは weeks が経験則上の落としどころです。
3-3. cacheTag による invalidation
import { cacheTag } from 'next/cache'
async function getProduct(id: string) {
'use cache'
cacheTag(`product-${id}`)
cacheTag('products') // 複数タグ可
return await db.products.findUnique({ where: { id } })
}
// Server Action から
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string) {
await db.products.update({ ... })
revalidateTag(`product-${id}`, 'max') // ピンポイント無効化(第2引数で expire profile を指定、v16 推奨形)
}
revalidatePath() と組み合わせるよりも、cacheTag をデータ階層に貼っておき、Server Action で revalidateTag する のが最もスケールします。
§4. 段階移行の実務手順(4ステップ)
4-1. ステップ1: feature flag で Cache Components を opt-in
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
// Next.js 16 で stable 化。PPR は cacheComponents: true で opt-in される設計
cacheComponents: true,
}
export default config
Next.js 16 では cacheComponents: true が PPR への opt-in を兼ねており、旧 experimental.ppr: 'incremental' のような併記は不要(v16 で削除済み)。同じ feature flag で PPR 移行と Cache Components 移行を 一緒に 進められるのが運用しやすい。
4-2. ステップ2: 静的ページから順に 'use cache' 移行
優先順位: 完全静的なマーケティングページ → ブログ記事 → 商品一覧 → 商品詳細 → カート / アカウント。
理由: 副作用が少ないページから移すほど事故が少ない。カート/アカウントは cookies/headers 必須なので最後。
4-3. ステップ3: 動的ページの fetch を 'use cache' 関数に切り出し
// Before
export default async function Page({ params }: { params: { id: string } }) {
const product = await fetch(`/api/products/${params.id}`, {
next: { revalidate: 3600, tags: [`product-${params.id}`] }
}).then(r => r.json())
return <ProductDetail product={product} />
}
// After
async function getProduct(id: string) {
'use cache'
cacheLife('hours')
cacheTag(`product-${id}`)
return await db.products.findUnique({ where: { id } })
}
export default async function Page({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return <ProductDetail product={product} />
}
ポイント: fetch のオプションでキャッシュ設定していたものを、関数定義側に移す。これだけ。
4-4. ステップ4: revalidatePath / revalidateTag を Server Action 側に集約
データ更新箇所を Server Action に集約し、各 Server Action の末尾で必要なタグを revalidate する設計にすると、「いつ何が無効化されるか」がコードから読める ようになります。
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, data: ProductInput) {
await db.products.update({ where: { id }, data })
revalidateTag(`product-${id}`, 'max') // 第2引数で expire profile を指定(v16 推奨形)
revalidateTag('products-list', 'max') // 一覧側も更新
}
ここまで来ると、ISR の revalidate: N が暗黙的に走るカオス状態から、明示的な invalidation グラフ へと切り替わります。
§5. 移行で踏みやすい5つの罠
5-1. next dev で動くが build で型エラー
'use cache' を付けた関数は、内部で 直接的・間接的に「シリアライズ不可能な値」を扱うとビルドエラー になります。具体的には関数、Symbol、class インスタンスなど(公式値)。Date / Map / Set は v16 で supported types に含まれます。
回避: return する値はプリミティブ + プレーンオブジェクト + 配列に限定。Date は ISO 文字列に変換、Map は配列に変換。
5-2. クッキー / ヘッダー読み出しは 'use cache' 内禁止
// NG: runtime error
async function getUserCart() {
'use cache'
const session = cookies().get('session') // ❌ 実行時エラー
return await db.carts.findUnique(...)
}
理由: cookies/headers はリクエストごとに変わる動的値。キャッシュ可能関数で読み出すと「誰のキャッシュ?」が決まらない。
回避: 「ユーザー依存しないデータ取得」と「ユーザー依存の表示」を分離。前者だけ 'use cache'、後者は通常の Server Component。
5-3. 旧 fetch({ cache: 'no-store' }) との衝突
'use cache' 関数の中で fetch({ cache: 'no-store' }) を呼ぶと、外側のキャッシュは効くが内側の fetch は毎回走る という、混乱の元になる挙動になります。
回避: 'use cache' 関数内では fetch オプションでキャッシュ制御しない。キャッシュ可否は関数側で1元管理。
5-4. Turbopack ビルドキャッシュとの relationship
next dev --turbo 環境で 'use cache' を変更しても、Turbopack のメモリキャッシュが古い結果を返すことがあります(経験則)。
回避: 変更が反映されない場合は .next ディレクトリを削除して dev server を再起動。'use cache' のロジックを変えた直後だけは next build && next start で確認。
5-5. ISR revalidate フラグとの設定上書き
旧 export const revalidate = 60 がページに残ったまま 'use cache' を導入すると、どちらの TTL が効いているか不明な状態 になります。
回避: 移行と同時に export const revalidate / export const dynamic は削除。Cache Components 移行 PR の必須チェック項目。
§6. 移行チェックリスト 12項目(offer)
| # | 項目 | 判定基準 | 担当 |
|---|---|---|---|
| 1 | App Router 100% 化 | Pages Router 0 ファイル | Tech Lead |
| 2 | PPR 運用実績 | stage で 1ヶ月以上 | Tech Lead |
| 3 | キャッシュ観測整備 | x-vercel-cache 集計可能 | DevOps |
| 4 | feature flag 導入 | cacheComponents: true | FE |
| 5 | 静的ページ移行完了 | マーケ・ブログ・利用規約等 | FE |
| 6 | 動的ページの fetch を関数化 | 全 fetch 呼び出し | FE |
| 7 | cacheLife プリセット明示 | 全 'use cache' 関数 | FE |
| 8 | cacheTag 階層設計 | 個別 + 集約タグ | FE |
| 9 | Server Action 集約 | データ更新は全て Server Action | FE |
| 10 | 旧 revalidate フラグ削除 | grep 0 件 | FE |
| 11 | cookies/headers 隔離 | 'use cache' 内 0 件 | FE |
| 12 | ロールバック手順 | feature flag off 1コミット | DevOps |
これをそのまま PR description に貼って、各項目に √ を付けながら進めるのが最短ルートです。
§7. ロールバック条件と CI ガード
7-1. ロールバック判断の3条件
- キャッシュヒット率が移行前を下回った状態が 24h 継続 ── 設計ミスの兆候
- LCP の P75 が悪化 ── stale が長すぎるか、無効化漏れ
- 5xx エラーが 0.1% 以上増加 ──
'use cache'内 runtime error の可能性
いずれか1つで feature flag off に戻し、原因調査を別ブランチで実施。
7-2. CI ガード GitHub Actions snippet
# .github/workflows/cache-components-guard.yml
name: Cache Components Guard
on: [pull_request]
jobs:
guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 旧 revalidate / dynamic フラグ検出
run: |
if grep -rn "export const revalidate" app/ ; then
echo "ERROR: legacy revalidate flag detected"
exit 1
fi
if grep -rn "export const dynamic" app/ ; then
echo "ERROR: legacy dynamic flag detected"
exit 1
fi
- name: cookies in use cache 検出
run: |
# 'use cache' を含むファイルで cookies/headers 呼び出しを検出
for f in $(grep -rln "'use cache'" app/) ; do
if grep -E "(cookies|headers)\(\)" "$f" ; then
echo "ERROR: cookies/headers in $f"
exit 1
fi
done
これを PR ごとに走らせれば、§5 の罠 5-2 / 5-5 を機械的に防げます。
7-3. PR テンプレ(コピペ可)
## Cache Components 移行 PR
### 影響範囲
- 対象ページ: app/products/[id]/page.tsx
- 対象関数: getProduct, getProductList
### チェックリスト
- [ ] §6 の12項目を確認した
- [ ] `'use cache'` に `cacheLife` プリセットを明示した
- [ ] `cacheTag` を data 階層に貼った
- [ ] cookies/headers を `'use cache'` 内で呼んでいない
- [ ] 旧 `export const revalidate` を削除した
- [ ] feature flag off で正常動作することを確認した
### ロールバック条件
- キャッシュヒット率 24h 悪化 / LCP P75 悪化 / 5xx 0.1% 増 のいずれか1つで feature flag off
§8. 関連記事と次のアクション
8-1. 3行サマリ
- Cache Components は PPR の延長線。
'use cache'を境界とする宣言的キャッシュモデル - 判定は Yes / Defer / No の3分岐。観測整備と PPR 運用経験が前提
- §6 のチェックリスト + §7 の CI ガード + PR テンプレでそのまま PoC を回せる
8-2. 関連記事
API 層の選定: tRPC を採用すべきか 型安全API設計の現実解 フロント性能の優先順位: フロント性能改善の正しい順序 ビルド時間の改善: CI/CD 速度最適化の打ち手 AI 駆動開発の品質ゲート: AI 時代の CI 品質ゲート設計
8-3. CTA: 移行 PoC 伴走
「自社の Next.js プロジェクトで Cache Components 移行 PoC を1日で回したい」「キャッシュヒット率の観測整備から伴走してほしい」「PR テンプレと CI ガードを社内向けにカスタムしたい」── いずれかに当てはまる方は、本記事のチェックリスト + 伴走支援の問い合わせを受け付けています。
X(旧 Twitter)のみね に DM をいただければ、記事診断という形で自社プロジェクトの状況に合わせた移行 PoC の道筋をお手伝いしています。
FAQ
Q1. Cache Components は本番投入できますか?
Next.js 16 で stable として提供(cacheComponents: true を next.config.ts のトップレベルに設定して opt-in)。cacheLife / cacheTag も v16 で stable 化済み。段階導入は stage 環境での検証 → 一部ルートで 'use cache' 適用 → 全面適用 の順がおすすめ。Tech Lead の判断で段階移行する設計が安全(公式値)。
Q2. PPR との違いを一言で?
PPR は 「レンダリング戦略」(静的シェル + 動的穴)、Cache Components は 「キャッシュモデル」('use cache' 境界)。両者は併用が前提で、Cache Components が PPR を置き換えるわけではない。
Q3. ISR から移行する手順は?
export const revalidate = Nの N を確認- データ取得関数を切り出して
'use cache'化 cacheLifeで同等 TTL を設定(任意の N 秒ならcacheLife({ revalidate: N })の custom profile、secondsプリセットは固定値で revalidate=1秒)export const revalidateを削除- 動作確認後、
cacheTagでピンポイント invalidation に切り替え
Q4. 既存の fetch({ next: { revalidate } }) は残せますか?
残せるが推奨しない。'use cache' 関数の外なら動作するが、二重キャッシュで挙動が読みにくくなる。移行 PR で全 fetch のキャッシュ設定を関数側に移すのが現実解。
Q5. Turbopack と相性は?
next dev --turbo で 'use cache' 自体は動作するが、変更検知が稀に遅延する(経験則)。dev での挙動確認は next build && next start で最終確認するのが安全。本番ビルドは Turbopack 関係なく動く。
鮮度メンテ計画
- 公開日: 2026-05-01
- 最終更新: 2026-05-01
- 対象バージョン: Next.js 16.x 系(cacheComponents stable、
cacheLife/cacheTagstable) - 次回見直し: 2026-07-01 もしくは Next.js 17 GA 公表時 / cacheLife プリセット追加時
- 追記対象: 新プリセット追加、Next.js 17 GA での挙動変更、Vercel ドキュメントの構造変更
関連記事
- RSCとClient境界の引き方 —
'use cache'を貼る前提となる「どの Component を Server にするか」の境界判断テンプレ。Cache Components 移行の前段として読むと、データフェッチ起点と境界設計が整合する。
