
AI Assisted
この記事は筆者の実装経験をもとに実装コードをベースで執筆し、AIによる校閲・推敲を経て公開しています。
Next.js 16で導入された "use cache" ディレクティブによる新しいキャッシュモデル。
従来の「ページ単位のISR」から「コンポーネント単位の明示的キャッシュ」へのパラダイムシフトです。
今回は、この新機能の解説と、実プロジェクト(nero15.dev)での移行作業で得た知見を共有します。
cacheComponents: true を有効にすると、従来の revalidate / dynamic は**使用不可(ビルドエラー)**になる。"use cache" + cacheLife() に書き換える必要がある。従来の「デフォルトでキャッシュされる(暗黙的)」挙動から、「明示的にキャッシュをオプトインする」方式への変更です。
// ページ全体の設定しかできない
export const revalidate = 3600
export default async function Page() {
const data = await fetch('/api/data') // キャッシュされる
const user = await getUser() // これもキャッシュされる?
return <div>{data}</div>
}
問題点:
// コンポーネント単位で制御
async function CachedSection() {
'use cache'
cacheLife('hours') // 1時間キャッシュ
const data = await fetchData()
return <div>{data}</div>
}
async function RealtimeSection() {
// デフォルトは動的(キャッシュなし)
const data = await fetchRealtimeData()
return <div>{data}</div>
}
メリット:
まずは機能を有効化します。
// next.config.js
const nextConfig = {
// Cache Components有効化
cacheComponents: true,
// カスタムキャッシュプロファイル(任意)
cacheLife: {
fiveMinutes: {
stale: 300, // 5分間はキャッシュをそのまま返す
revalidate: 300, // 5分後にバックグラウンド再検証
expire: 3600, // 1時間で強制期限切れ
},
oneDay: {
stale: 86400, // 1日間
revalidate: 86400,
expire: 604800, // 1週間で強制期限切れ
},
},
}
Next.jsが標準で用意しているプロファイルです。基本はこれらを使えば十分です。
| プロファイル | キャッシュ期間 | 想定ユースケース |
|---|---|---|
'seconds' | 短期 | リアルタイム性重視のデータ |
'minutes' | 5分 | 記事一覧、コメント |
'hours' | 1時間 | 記事詳細、ランキング |
'days' | 1日 | カテゴリ一覧、タグ一覧 |
'weeks' | 1週間 | 統計データ、アーカイブ |
'max' | 無期限 | 静的コンテンツ(Aboutページ等) |
// src/components/home/LatestPosts.tsx
import { cacheLife } from 'next/cache'
export default async function LatestPosts() {
'use cache'
cacheLife('minutes') // 5分キャッシュ
const posts = await fetchLatestPosts()
return (
<div className="grid grid-cols-3 gap-6">
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}
データ取得関数そのものをキャッシュすることも可能です。
// src/lib/api/publicApi.ts
import { cacheLife } from 'next/cache'
export async function fetchPosts(params = {}) {
'use cache'
cacheLife('minutes')
const response = await fetch('/api/posts?' + new URLSearchParams(params))
return response.json()
}
PPR(Partial Prerendering)を使うと、1つのページ内で静的部分と動的部分を共存させられます。
構成イメージ:
┌─────────────────────────────────────┐
│ ヘッダー(静的・即座に表示) │
├─────────────────────────────────────┤
│ 記事本文(静的・即座に表示) │
├─────────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 関連記事(動的・後から表示) │
│ │ Loading... │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
実装コード:
// src/app/posts/[id]/page.tsx
import { Suspense } from 'react'
import { fetchPost } from '@/lib/api/publicApi'
import RelatedPosts from '@/components/posts/RelatedPosts'
import RelatedPostsSkeleton from '@/components/posts/RelatedPostsSkeleton'
export default async function PostPage({ params }) {
const { id } = await params
const post = await fetchPost(id)
return (
<article>
{/* 静的部分: ビルド時にプリレンダリングされ、TTFBが高速 */}
<h1>{post.title}</h1>
<div>{post.body}</div>
{/* 動的部分: Suspense境界で囲み、ストリーミング配信 */}
<Suspense fallback={<RelatedPostsSkeleton />}>
<RelatedPosts tag={post.tags?.[0]} excludeId={post.id} />
</Suspense>
</article>
)
}
// src/components/posts/RelatedPosts.tsx
import { cacheLife } from 'next/cache'
export default async function RelatedPosts({ tag, excludeId }) {
'use cache'
cacheLife('days') // 関連記事は頻繁に変わらないので長くキャッシュ
const posts = await fetchRelatedPosts(tag, excludeId)
// ...
}
ここが最大の難関です。next.config.js で cacheComponents: true を有効にした瞬間、プロジェクト内の全ての revalidate / dynamic 設定がエラーになります。
Route segment config "revalidate" is not compatible with `nextConfig.cacheComponents`.
Please remove it.
つまり、1ファイルずつ移行することはできず、全ファイルを一括で書き換える必要があります。
| 従来の設定 (Next.js 15) | 新しい設定 (Next.js 16) |
|---|---|
export const revalidate = 300 | コンポーネント内で cacheLife('minutes') |
export const revalidate = 3600 | コンポーネント内で cacheLife('hours') |
export const revalidate = 86400 | コンポーネント内で cacheLife('days') |
export const dynamic = 'force-dynamic' | 何も書かない(デフォルトで動的) |
# 修正が必要なファイルをリストアップ
grep -r "export const revalidate" src/app/
grep -r "export const dynamic" src/app/
各ページファイルから export const revalidate を削除し、データ取得を行っているコンポーネントまたは関数に 'use cache' と cacheLife を追加します。
// Before: src/app/page.tsx
export const revalidate = 300 // 削除
export default async function Page() {
const posts = await fetchPosts()
return <PostList posts={posts} />
}
// After: src/app/page.tsx
import LatestPosts from '@/components/home/LatestPosts'
// Pageコンポーネント自体はキャッシュせず、中のコンポーネントで制御
export default function Page() {
return <LatestPosts />
}
// After: src/components/home/LatestPosts.tsx
import { cacheLife } from 'next/cache'
export default async function LatestPosts() {
'use cache'
cacheLife('minutes') // ここで制御
// ...
}
API Route (Route Handlers) も同様です。unstable_cache を使うか、処理関数自体に 'use cache' を付けます。
全ての修正が終わったら、最後に next.config.js を更新します。
const nextConfig = {
cacheComponents: true,
}
use cache 内でのAPI制限'use cache' スコープ内では、リクエストごとの動的データ(Cookie, Headers, SearchParams)には直接アクセスできません。
// ❌ NG
async function UserProfile() {
'use cache'
const cookieStore = await cookies() // エラー!
}
// ✅ OK: 引数として受け取る
async function UserProfile({ userId }) {
'use cache'
// ...
}
// 呼び出し元でCookieを取得して渡す
export default async function Page() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
return <UserProfile userId={userId} />
}
データ更新(Mutation)を行った際、自動でキャッシュはクリアされません。revalidatePath や revalidateTag を手動で呼び出す必要があります。
import { revalidateTag } from 'next/cache'
async function updatePost(id, data) {
await db.update(id, data)
revalidateTag('posts') // 必須
}
Next.js 16の Cache Components は、これまでの「分かりにくいキャッシュ」を一掃する素晴らしい変更です。 一括移行のハードルはありますが、一度移行してしまえば**「明示的で予測可能なキャッシュ制御」と「PPRによる高度なUX」**が手に入ります。
Next.js 17以降ではこの方式が標準になる予定ですので、今のうちに移行を進めておくことを強くおすすめします。
スポーツ×ITの会社でバックエンドエンジニア兼マネージャーとして勤務。インテル関連の情報を中心に、AI・IT技術やサイト運用ノウハウも発信しています。
最終更新: 2026年1月13日
© 2025 nero15.dev. All rights reserved.