Next.jsのキャッシュが効かない本当の理由(2026年完全解決ガイド)
Next.jsで開発していて、こんな経験ありませんか?「データ更新したのに変わらない...」「revalidate設定したのに動かない...」「ローカルでは動くのに本番で動かない...」実は、これはNext.js開発者なら誰もが通る道です。App Routerのキャッシュは非常に強力ですが、その分だけ混乱しやすい部分でもあります。
この記事では、Next.js 15・16のキャッシュレイヤーを一つずつ解説し、キャッシュがおかしな動作をする原因と、実際に使える解決策をまとめます。
Next.jsキャッシュの4つのレイヤーを理解しよう
デバッグを始める前に、まず知っておくべきことがあります。Next.js App Routerのキャッシュは1つではなく、4つの独立したレイヤーで構成されています。それぞれ異なる役割を担っており、これを混同するとキャッシュ問題の解決が難しくなります。
1. Request Memoization(リクエストメモ化)
スコープ: 単一レンダリングパス(サーバーサイドのみ)
ライフタイム: 単一リクエストの期間
目的: 単一レンダリング内で同一のデータフェッチを重複排除
// この2つのfetch呼び出しは自動的に重複排除されます // 実際には1つのHTTPリクエストのみが発生します async function ProductDetails({ id }: { id: string }) { const product = await fetch(`/api/products/${id}`); return <ProductPrice productId={id} />; } async function ProductPrice({ productId }: { productId: string }) { // この同じfetchはメモ化されます—重複リクエストなし const product = await fetch(`/api/products/${productId}`); return <span>${product.price}</span>; }
Request Memoizationは、単一のサーバーレンダリング中に同じURLとオプションを持つfetch呼び出しに対して自動的に発生します。これはNext.jsが拡張したReactのネイティブな動作です。
よくある誤解: これは永続的なキャッシュではありません。リクエストが完了すると、このメモ化は消えます。同じレンダーツリーの走査内でのみ重複フェッチを防ぎます。
2. Data Cache(データキャッシュ)
スコープ: サーバーサイド、永続的
ライフタイム: 再検証または手動無効化まで
目的: リクエストとデプロイ全体でfetchレスポンスをキャッシュ
// 無期限にキャッシュ(静的動作) const data = await fetch('https://api.example.com/data'); // 60秒間キャッシュ const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } }); // キャッシュしない const data = await fetch('https://api.example.com/data', { cache: 'no-store' });
Data Cacheは最も混乱が生じる場所です。fetch呼び出しの生のレスポンスを保存し、以下全体で永続化されます:
- 複数のユーザーリクエスト
- サーバーの再起動(適切なインフラストラクチャを持つ本番環境で)
- デプロイ(Vercelなどのプラットフォームで)
3. Full Route Cache(フルルートキャッシュ)
スコープ: サーバーサイド、永続的
ライフタイム: 再検証まで
目的: ルートの完全なHTMLとRSC(React Server Component)ペイロードをキャッシュ
Next.jsアプリケーションをビルドすると、静的ルートはビルド時に事前レンダリングされます。Full Route Cacheは以下を保存します:
- 初期ページロード用のレンダリングされたHTML
- クライアントサイドナビゲーション用のRSCペイロード
// このページは静的に生成され、完全にキャッシュされます export default function AboutPage() { return <div>会社概要</div>; } // このページはFull Route Cacheから除外されます export const dynamic = 'force-dynamic'; export default function DashboardPage() { return <div>ダッシュボード: {new Date().toISOString()}</div>; }
4. Router Cache(ルーターキャッシュ、クライアントサイド)
スコープ: クライアントサイド、セッション単位
ライフタイム: 自動無効化を伴うセッションベース
目的: 即座の戻る/進むナビゲーションのために訪問したルートセグメントをブラウザにキャッシュ
ユーザーが/productsを訪問 → Router Cacheが/productsのレイアウト+ページを保存 ユーザーが/products/123にナビゲート → Router Cacheが/products/123のページを保存 → /productsのレイアウトはキャッシュから再利用 ユーザーが戻るボタンをクリック → /productsページがRouter Cacheから即座に提供
Router CacheはNext.js 15で大きく変更され、最も混乱しやすい部分です。詳しくは後述します。
キャッシュが動かないときのデバッグ手順
データが更新されないときは、以下の順序で確認してみてください:
ステップ1:どのキャッシュレイヤーが関係しているかを特定
以下の質問を自分に問いかけてください:
| 質問 | はいの場合 | いいえの場合 |
|---|---|---|
| 古いデータが最初のロードですぐに表示されますか? | Data CacheまたはFull Route Cache | Router Cache(クライアントサイド) |
| 強制リロード(Cmd+Shift+R)で解決しますか? | Router Cache | サーバーサイドキャッシュ |
| 再デプロイで解決しますか? | Full Route Cache | Data Cacheの設定ミス |
| 開発環境ですか? | 開発サーバーはキャッシュが異なる | 本番ビルドを確認 |
ステップ2:Fetch設定を確認
最も一般的な問題は、fetchキャッシュのデフォルトを誤解していることです。
// ❌ よくある間違い:これが動的だと仮定する const res = await fetch('https://api.example.com/user'); // これはNext.jsではデフォルトでキャッシュされます! // ✅ 正しい方法:明示的にキャッシュから除外 const res = await fetch('https://api.example.com/user', { cache: 'no-store' }); // ✅ または時間ベースの再検証を使用 const res = await fetch('https://api.example.com/user', { next: { revalidate: 0 } // 毎回のリクエストで再検証 });
ステップ3:Route Segment Configを理解する
Route segment設定は、ルート全体がどのようにキャッシュされるかに影響します:
// app/dashboard/page.tsx // ルート全体を動的に強制 export const dynamic = 'force-dynamic'; // 静的生成を強制(動的関数が使用されるとエラー) export const dynamic = 'force-static'; // ルート全体の再検証を設定 export const revalidate = 60; // 60秒ごとに再検証 // すべてのキャッシュを無効化 export const revalidate = 0; export const fetchCache = 'force-no-store';
ドキュメントにない落とし穴
落とし穴 #1:cookies()とheaders()は自動的に除外
Server Componentで動的関数を使用すると、ルート全体が動的になります:
import { cookies } from 'next/headers'; export default async function UserPage() { // cookies()を呼び出すだけでこのルートは動的になります // 結果を使用しなくても! const cookieStore = await cookies(); // このfetchも動的になります const user = await fetch('/api/user'); return <div>{user.name}</div>; }
解決策: Cookieが必要だがキャッシュも必要な場合は、コンポーネントを再構築してください:
// 動的部分と静的部分を分離 import { Suspense } from 'react'; export default function UserPage() { return ( <div> <StaticHeader /> {/* この部分はキャッシュされる */} <Suspense fallback={<Loading />}> <DynamicUserContent /> {/* これだけが動的 */} </Suspense> </div> ); }
落とし穴 #2:searchParamsのパラドックス
ページでsearchParamsにアクセスすると、URLパラメータを使用しなくても動的になります:
// ❌ 検索パラメータが渡されなくてもこのページは動的 export default function ProductsPage({ searchParams, }: { searchParams: { sort?: string }; }) { // propsにsearchParamsが存在するだけで = 動的 return <ProductGrid />; } // ✅ より良い方法:必要な時だけsearchParamsにアクセス export default function ProductsPage() { return <ProductGrid />; // デフォルトで静的 } // フィルタリングされたビューには、別のルートまたはクライアントコンポーネントを使用
落とし穴 #3:POSTリクエストとData Cache
POSTリクエストには、多くの開発者を困惑させる独特のキャッシュ動作があります:
// POSTリクエストはデフォルトでキャッシュされません const res = await fetch('/api/data', { method: 'POST', body: JSON.stringify({ id: 1 }), }); // しかし、レスポンスがキャッシュヘッダーを使用すれば、キャッシュされる可能性があります // APIレスポンスヘッダーを確認してください!
落とし穴 #4:開発環境と本番環境の差異
これは最も厄介な落とし穴かもしれません。開発環境では:
- Data Cacheはデフォルトで無効
- Full Route Cacheは存在しない
- ホットリロードが予期せずキャッシュをクリアすることがある
# 常に本番ビルドでキャッシュ動作をテストしてください npm run build && npm start
キャッシュ問題が本番環境でのみ発生する場合、これが理由です。
落とし穴 #5:ISRとrevalidateのタイミング
revalidateは「N秒ごとにリフレッシュ」を意味するのではありません—「N秒後、次のリクエストがバックグラウンド再生成をトリガーする」を意味します:
export const revalidate = 60; // タイムライン: // t=0: ページ生成、キャッシュ // t=30: ユーザー訪問 → キャッシュ版を取得(30秒経過) // t=65: ユーザー訪問 → キャッシュ版を取得(65秒経過)、バックグラウンド再生成トリガー // t=66: 新しいキャッシュ準備完了 // t=70: ユーザー訪問 → 新鮮なバージョンを取得
stale-while-revalidateパターンは、ユーザーがrevalidate期間後も古いコンテンツを見る可能性があることを意味します。
実践で使える解決パターン
レシピ1:リアルタイムダッシュボードデータ
問題: 更新後もダッシュボードが古いデータを表示する。
// app/dashboard/page.tsx import { unstable_noStore as noStore } from 'next/cache'; export default async function Dashboard() { noStore(); // このコンポーネントのすべてのキャッシュを除外 const stats = await fetchDashboardStats(); return <DashboardView stats={stats} />; } // またはroute segment configを使用 export const dynamic = 'force-dynamic'; export const revalidate = 0;
レシピ2:共有レイアウトとユーザー固有のコンテンツ
問題: レイアウトはキャッシュされているが、ページコンテンツはユーザー固有である必要がある。
// app/dashboard/layout.tsx // このレイアウトは静的のまま維持できます export default function DashboardLayout({ children }) { return ( <div className="dashboard-container"> <Sidebar /> {/* キャッシュされる */} {children} </div> ); } // app/dashboard/page.tsx import { cookies } from 'next/headers'; export default async function DashboardPage() { const cookieStore = await cookies(); const userId = cookieStore.get('userId')?.value; // このページは動的ですが、レイアウトはまだキャッシュされています const userData = await fetch(`/api/users/${userId}`, { cache: 'no-store' }); return <UserDashboard data={userData} />; }
レシピ3:価格更新が必要なECサイトの商品ページ
問題: 価格変更は5分以内に反映される必要があるが、商品説明はより長くキャッシュできる。
// app/products/[id]/page.tsx export const revalidate = 300; // 5分 export default async function ProductPage({ params }) { // 商品詳細は5分ごとに再検証 const product = await fetch(`/api/products/${params.id}`, { next: { revalidate: 300 } }); // 在庫はリアルタイムである必要がある const inventory = await fetch(`/api/inventory/${params.id}`, { cache: 'no-store' }); return <ProductView product={product} inventory={inventory} />; }
レシピ4:オンデマンド再検証を使用したブログ
問題: ブログ投稿はキャッシュされるべきだが、CMSでコンテンツが更新されたら再検証する。
// app/blog/[slug]/page.tsx export const revalidate = false; // 無期限にキャッシュ export default async function BlogPost({ params }) { const post = await fetch(`/api/posts/${params.slug}`, { next: { tags: [`post-${params.slug}`] } }); return <Article post={post} />; } // app/api/revalidate/route.ts import { revalidateTag } from 'next/cache'; import { NextRequest } from 'next/server'; export async function POST(request: NextRequest) { const { slug, secret } = await request.json(); if (secret !== process.env.REVALIDATION_SECRET) { return Response.json({ error: '無効なシークレット' }, { status: 401 }); } revalidateTag(`post-${slug}`); return Response.json({ revalidated: true }); }
レシピ5:Router Cacheの問題を修正する(Next.js 15/16)
問題: クライアントサイドナビゲーションが古いデータを表示する。
Next.js 15以降、Router Cacheの動作が大幅に変更されました。動的ページはデフォルトでクライアントにキャッシュされなくなりましたが、静的ページはまだキャッシュされます。Next.js 16でもこのアプローチはさらに改良されて継続されています。
// 毎回のナビゲーションで新鮮なデータが必要な動的ルートの場合: import { useRouter } from 'next/navigation'; function RefreshButton() { const router = useRouter(); const handleRefresh = () => { router.refresh(); // 現在のルートのRouter Cacheを無効化 }; return <button onClick={handleRefresh}>データを更新</button>; }
より積極的なキャッシュ破棄の場合:
// middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const response = NextResponse.next(); // 特定のルートでRouter Cacheを防止 if (request.nextUrl.pathname.startsWith('/dashboard')) { response.headers.set( 'Cache-Control', 'no-store, must-revalidate' ); } return response; }
高度なパターン:キャッシュタグ戦略
キャッシュタグを使用すると、キャッシュされたデータ間の関係を作成し、正確に無効化できます:
// lib/data.ts export async function getProducts(category: string) { return fetch(`/api/products?category=${category}`, { next: { tags: [ 'products', // すべての商品 `category-${category}`, // 特定のカテゴリ ] } }); } export async function getProductById(id: string) { return fetch(`/api/products/${id}`, { next: { tags: [ 'products', `product-${id}`, ] } }); } // 単一の商品が更新された場合: revalidateTag(`product-${updatedProductId}`); // カテゴリが再編成された場合: revalidateTag(`category-${categoryName}`); // 核オプション - すべての商品: revalidateTag('products');
デバッグツールとテクニック
1. キャッシュヘッダーの検査
// 開発環境でキャッシュデバッグヘッダーを追加 // next.config.js module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'x-next-cache-status', value: process.env.NODE_ENV === 'development' ? 'disabled' : 'enabled', }, ], }, ]; }, };
2. キャッシュ動作のロギング
// lib/fetch-with-logging.ts export async function fetchWithCacheLogging(url: string, options?: RequestInit) { const startTime = performance.now(); const response = await fetch(url, options); console.log({ url, cacheMode: options?.cache ?? 'default', revalidate: (options as any)?.next?.revalidate ?? '未設定', responseTime: `${(performance.now() - startTime).toFixed(2)}ms`, fromCache: response.headers.get('x-cache') === 'HIT', }); return response; }
3. 視覚的なキャッシュインジケーター
// components/CacheDebugBadge.tsx import { unstable_cache } from 'next/cache'; export function CacheDebugBadge() { if (process.env.NODE_ENV !== 'development') return null; const renderTime = new Date().toISOString(); return ( <div className="fixed bottom-4 right-4 bg-black text-white p-2 text-xs rounded"> レンダリング時刻: {renderTime} </div> ); }
Next.js 15/16キャッシュの変更点:何が新しくなったのか
Next.js 15はキャッシュのデフォルトにいくつかの重要な変更を導入し、Next.js 16(2025年10月リリース)は完全に新しいアプローチでキャッシュを革新しました。
Next.js 15の変更点(依然として有効)
1. fetchリクエストがデフォルトでキャッシュされなくなった
// Next.js 14以前: デフォルトでキャッシュ // Next.js 15以降: デフォルトでキャッシュされない // 以前の動作を復元するには: const data = await fetch(url, { cache: 'force-cache' });
2. Route Handlerがデフォルトで動的に
// Next.js 14以前: GETハンドラーがキャッシュされた // Next.js 15以降: すべてのハンドラーがデフォルトで動的 // route handlerを静的にするには: export const dynamic = 'force-static'; export async function GET() { return Response.json({ time: new Date().toISOString() }); }
3. クライアントRouter Cacheの変更
// Next.js 14以前: ページが30秒(動的)または5分(静的)キャッシュ // Next.js 15以降: 動的ページは0秒のstaleness time // staleness timeを設定するには: // next.config.js module.exports = { experimental: { staleTimes: { dynamic: 30, // 秒 static: 180, // 秒 }, }, };
Next.js 16: "use cache" 革命
Next.js 16は"use cache"ディレクティブと共にCache Componentsを導入しました—App Router導入以来最も重要なキャッシュの変化です。これはPartial Pre-Rendering (PPR)のビジョンを完成させます。
パラダイムシフト:暗黙的から明示的へ
Next.js 16では、すべての動的コードがデフォルトでリクエスト時に実行されます。キャッシュは完全にopt-inになりました:
// Next.js 16: このページはデフォルトで動的です export default async function ProductPage({ params }) { const product = await fetch(`/api/products/${params.id}`); return <ProductView product={product} />; } // キャッシュするには、明示的に"use cache"を使用する必要があります "use cache" export default async function ProductPage({ params }) { const product = await fetch(`/api/products/${params.id}`); return <ProductView product={product} />; }
異なるレベルでの"use cache"の使用
// ページ全体をキャッシュ "use cache" export default async function BlogPostPage({ params }) { const post = await fetchPost(params.slug); return <Article post={post} />; } // 特定のコンポーネントをキャッシュ async function CachedSidebar() { "use cache" const categories = await fetchCategories(); return <Sidebar categories={categories} />; } // 関数をキャッシュ async function getExpensiveData(id: string) { "use cache" // この結果はキャッシュされます return await computeExpensiveData(id); }
キャッシュタグとの組み合わせ
"use cache" import { cacheTag } from 'next/cache'; export default async function ProductPage({ params }) { cacheTag(`product-${params.id}`); const product = await fetch(`/api/products/${params.id}`); return <ProductView product={product} />; } // revalidateTagで無効化 import { revalidateTag } from 'next/cache'; revalidateTag(`product-${params.id}`);
新機能:cacheLifeによる有効期限制御
"use cache" import { cacheLife } from 'next/cache'; export default async function DashboardStats() { cacheLife('minutes'); // 事前定義されたプロファイル: 'seconds', 'minutes', 'hours', 'days', 'weeks', 'max' const stats = await fetchStats(); return <StatsView stats={stats} />; } // またはカスタム値で cacheLife({ stale: 60, // 60秒間staleを提供 revalidate: 300, // 5分後に再検証 expire: 3600, // 1時間後に期限切れ });
Turbopackファイルシステムキャッシュ(ベータ)
Next.js 16はファイルシステムキャッシュと共にTurbopackをデフォルトのバンドラーにしました:
// next.config.js module.exports = { experimental: { turbo: { // より高速なリビルドのためのファイルシステムキャッシュ persistentCaching: true, }, }, };
これにより、コンパイラアーティファクトを実行間でディスクに保存することで、起動時間とコンパイル時間が劇的に改善されます。
パフォーマンスへの影響:何をいつキャッシュすべきか
キャッシュを理解することは、バグを避けるだけでなく、パフォーマンスを最適化することでもあります:
| コンテンツタイプ | 推奨戦略 | 理由 |
|---|---|---|
| マーケティングページ | revalidate: 3600またはビルド時の静的 | 変更が少なく、CDNヒットを最大化 |
| ECサイトのリスト | revalidate: 60 + オンデマンド | 鮮度とパフォーマンスのバランス |
| ユーザーダッシュボード | dynamic = 'force-dynamic' | ユーザー固有、新鮮なデータが必要 |
| APIルート(公開) | revalidate: 300 + キャッシュタグ | バックエンドの負荷軽減 |
| APIルート(認証) | cache: 'no-store' | セキュリティと鮮度 |
| リアルタイムデータ | クライアントサイドフェッチ | サーバーキャッシュは不適切 |
よくあるエラーメッセージと解決策
"DYNAMIC_SERVER_USAGE"
Error: Dynamic server usage: cookies
解決策: 静的エクスポートルートで動的関数を使用しています。以下のいずれかを行います:
- 動的関数を削除
export const dynamic = 'force-dynamic'を追加- 動的ロジックをクライアントコンポーネントに移動
"Invariant: static generation store missing"
これは通常、ビルド中に動的APIを使用しようとしていることを示します:
// ❌ 問題 export async function generateStaticParams() { const cookieStore = await cookies(); // ここでは使用できません! return []; } // ✅ 解決策: generateStaticParamsでは静的データのみを使用 export async function generateStaticParams() { const products = await fetch('/api/products', { cache: 'force-cache' }).then(r => r.json()); return products.map(p => ({ id: p.id })); }
Vercelでキャッシュが無効化されない
// 正しい再検証APIを使用していることを確認 import { revalidatePath, revalidateTag } from 'next/cache'; // revalidatePathはパスのすべてのデータを無効化 revalidatePath('/products'); // revalidateTagはより外科的 revalidateTag('products-list'); // 両方とも同じデプロイからリクエストが発生する必要がある // CMSからWebhook → APIルート → revalidate関数を使用
結論:Next.jsキャッシュのためのメンタルモデル
これらすべてを理解した後、持ち帰るべきメンタルモデルは以下の通りです:
-
Next.js 15/16ではデフォルトで動的に。 キャッシュなしで始め、有益な場所に追加してください。予期しないキャッシュヒットをデバッグするよりも良いです。
-
静的と動的を分離する。 Suspense境界とコンポーネント構成を使用して、キャッシュ可能なコンテンツを最大化してください。
-
適切なレベルでキャッシュする。 特定のフェッチに対するData Cacheで十分な場合、Full Route Cacheを使用しないでください。
-
常に本番ビルドでテストする。 開発サーバーはキャッシュ動作について嘘をつきます。
-
精度のためにキャッシュタグを使用する。 複雑なアプリではパスベースの無効化よりも保守性が高いです。
-
本番環境でキャッシュ動作を監視する。 ロギングを追加し、Vercel Analyticsを使用するか、カスタムキャッシュヘッダーを実装してください。
Next.jsのキャッシュが複雑なのは、複雑な問題を解決しているからです:大規模で高速かつ新鮮なコンテンツを配信すること。4つのレイヤーとその相互作用を理解すれば、驚くほど高速でありながら確実に最新のアプリケーションを構築できます。
キーは、キャッシュと戦うのではなく、一緒に働くことです—各レイヤーを特定のユースケースに適切に設定し、時には最良のキャッシュ設定がまったくキャッシュしないことだと理解することです。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう