🎉
【Next.js完全ガイド Vol.4】4層のキャッシング戦略を完全マスター
【Next.js完全ガイド Vol.4】4層のキャッシング戦略を完全マスター
はじめに
この記事は「Next.js完全ガイド」シリーズの第4回です。
📚 シリーズ構成
- 第1回: Next.js 15最新情報 & アーキテクチャの基礎
- 第2回: レンダリング戦略の完全理解
- 第3回: データフェッチング戦略
- 第4回: キャッシング戦略 ← 今ここ
- 第5回: エラーハンドリング・メタデータ・実践パターン
前回のおさらい
前回は、Next.jsのデータフェッチング戦略を学びました:
- Server Componentsでの4つのキャッシングパターン
- Route Handlersによる REST API実装
- Server Actionsでのフォーム送信とデータ変更
- SWRを使った高度なクライアントサイドフェッチング
この記事で学べること
- Next.jsの4つのキャッシュ層の仕組み
- Request Memoization、Data Cache、Full Route Cache、Router Cacheの違い
- Next.js 14と15のキャッシング動作の違い
- 実践的なキャッシング戦略
- キャッシュのデバッグ方法
1. Next.jsにおける4つのキャッシュ層
Next.jsは、パフォーマンスを最適化するために複数のレイヤーでキャッシングを行います。
┌─────────────────────────────────────────┐
│ 1. Request Memoization │
│ スコープ: 単一のサーバーリクエスト │
│ 期間: リクエストのライフサイクル │
│ 場所: サーバー(メモリ内) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 2. Data Cache │
│ スコープ: 複数のリクエスト │
│ 期間: 永続的(再デプロイまで) │
│ 場所: サーバー(ファイルシステム) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 3. Full Route Cache │
│ スコープ: 静的ルート全体 │
│ 期間: 永続的(再デプロイまで) │
│ 場所: サーバー(ファイルシステム) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 4. Router Cache (クライアント側) │
│ スコープ: ユーザーセッション │
│ 期間: セッション中または時間ベース │
│ 場所: ブラウザ(メモリ内) │
└─────────────────────────────────────────┘
2. Request Memoization(リクエストメモ化)
概念
同じサーバーリクエスト内で、同じURLと同じオプションのfetchを複数回呼んでも、実際には1回しか実行されません。
実装例
// app/products/[id]/page.js
export default async function ProductPage({ params }) {
// 同じfetchが3回
const product1 = await fetch(`https://api.example.com/products/${params.id}`);
const product2 = await fetch(`https://api.example.com/products/${params.id}`);
const product3 = await fetch(`https://api.example.com/products/${params.id}`);
// でも実際のネットワークリクエストは1回だけ!
// 2回目と3回目はメモリから取得
return <ProductDetails product={product1} />;
}
動作タイムライン
リクエスト開始
↓
fetch #1実行 → API呼び出し(200ms)
↓
fetch #2実行 → メモリから取得(<1ms)
↓
fetch #3実行 → メモリから取得(<1ms)
↓
合計: 200ms(従来なら600ms)
実用例:複数コンポーネントでの同じデータ使用
// app/products/[id]/page.js
export default async function ProductPage({ params }) {
return (
<div>
<ProductHeader productId={params.id} />
<ProductDetails productId={params.id} />
<ProductReviews productId={params.id} />
</div>
);
}
// 各コンポーネントで同じfetchを呼んでも1回しか実行されない
async function ProductHeader({ productId }) {
const product = await fetch(`https://api.example.com/products/${productId}`)
.then(r => r.json());
return <h1>{product.name}</h1>;
}
async function ProductDetails({ productId }) {
const product = await fetch(`https://api.example.com/products/${productId}`)
.then(r => r.json());
return <div>{product.description}</div>;
}
async function ProductReviews({ productId }) {
const product = await fetch(`https://api.example.com/products/${productId}`)
.then(r => r.json());
return <div>評価: {product.rating}</div>;
}
重要な特性
- ✅ 自動的に動作(設定不要)
- ✅ GET リクエストのみ
- ✅ 同じリクエスト内でのみ有効
- ✅ リクエスト完了後はクリア
3. Data Cache(データキャッシュ)
概念
fetchの結果を永続的に保存し、複数のリクエスト間で再利用します。Next.js 15からデフォルトでオフになりました。
Next.js 14 vs 15の違い
// Next.js 14: デフォルトでキャッシュされる
const data = await fetch('https://api.example.com/data');
// → 初回のみAPI呼び出し、以降はキャッシュから取得
// Next.js 15: デフォルトでキャッシュされない
const data = await fetch('https://api.example.com/data');
// → 毎回API呼び出し
// Next.js 15: 明示的にキャッシュ
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache'
});
// → 初回のみAPI呼び出し、以降はキャッシュから取得
キャッシュオプション
// 1. キャッシュしない(デフォルト in Next.js 15)
fetch(url, { cache: 'no-store' })
// 2. 永続的にキャッシュ
fetch(url, { cache: 'force-cache' })
// 3. 時間ベースの再検証
fetch(url, { next: { revalidate: 3600 } }) // 1時間
// 4. タグベースの再検証
fetch(url, { next: { tags: ['products'] } })
キャッシュの再検証(Revalidation)
時間ベースの再検証
// 60秒ごとに自動更新
export default async function NewsPage() {
const articles = await fetch('https://api.news.com/articles', {
next: { revalidate: 60 }
}).then(r => r.json());
return (
<div>
{articles.map(article => (
<Article key={article.id} {...article} />
))}
</div>
);
}
タイムライン:
10:00:00 - ユーザーA訪問
↓
キャッシュなし → API呼び出し → キャッシュ保存
10:00:30 - ユーザーB訪問
↓
キャッシュヒット(即座に表示)
10:01:15 - ユーザーC訪問
↓
キャッシュ期限切れ
→ 古いキャッシュ表示(stale-while-revalidate)
→ バックグラウンドで再検証
10:01:20 - ユーザーD訪問
↓
新しいキャッシュヒット
オンデマンド再検証(タグベース)
// データ取得時
export default async function ProductsPage() {
const products = await fetch('https://api.shop.com/products', {
next: { tags: ['products', 'homepage'] }
}).then(r => r.json());
return <ProductGrid products={products} />;
}
// 管理画面でデータ更新時
// app/api/admin/products/route.js
import { revalidateTag } from 'next/cache';
export async function POST(request) {
const body = await request.json();
// データベースを更新
await updateProduct(body);
// 'products'タグ付きキャッシュを即座に無効化
revalidateTag('products');
return Response.json({ success: true });
}
パスベースの再検証
// app/api/admin/products/route.js
import { revalidatePath } from 'next/cache';
export async function POST(request) {
await updateProduct(await request.json());
// 特定のパスのキャッシュを無効化
revalidatePath('/products');
revalidatePath('/'); // トップページも無効化
return Response.json({ success: true });
}
複数タグの使用
export default async function ProductPage({ params }) {
const product = await fetch(`https://api.shop.com/products/${params.id}`, {
next: {
tags: ['products', `product-${params.id}`, 'homepage']
}
}).then(r => r.json());
return <ProductDetails product={product} />;
}
// 特定の商品だけ更新
revalidateTag(`product-${productId}`);
// 全商品を更新
revalidateTag('products');
// トップページも更新
revalidateTag('homepage');
4. Full Route Cache(フルルートキャッシュ)
概念
静的レンダリングされたルート全体(HTMLとRSC Payload)をビルド時に生成し、保存します。
動作フロー
ビルド時
↓
ルート分析
↓
静的か動的か判定
↓
【静的ルートの場合】
↓
HTML生成
↓
RSC Payload生成
↓
ファイルとして保存
└─ .next/server/app/about.html
└─ .next/server/app/about.rsc
↓
デプロイ後は超高速配信🚀
静的 vs 動的の自動判定
// 静的レンダリング(Full Route Cacheが有効)
export default function AboutPage() {
return (
<div>
<h1>会社概要</h1>
<p>私たちは...</p>
</div>
);
}
// 動的レンダリング(Full Route Cacheが無効)
import { cookies } from 'next/headers';
export default async function DashboardPage() {
const cookieStore = cookies(); // cookies()を使用
const user = await getCurrentUser();
return (
<div>
<h1>こんにちは、{user.name}さん</h1>
</div>
);
}
動的になる条件
// 1. cookies()の使用
import { cookies } from 'next/headers';
const cookieStore = cookies();
// 2. headers()の使用
import { headers } from 'next/headers';
const headersList = headers();
// 3. searchParamsの使用
export default function SearchPage({ searchParams }) {
const query = searchParams.q;
}
// 4. cache: 'no-store'のfetch
fetch(url, { cache: 'no-store' });
// 5. revalidate: 0
fetch(url, { next: { revalidate: 0 } });
明示的な制御
// ページ全体を動的にする
export const dynamic = 'force-dynamic';
export default function Page() {
return <div>このページは常に動的</div>;
}
// ページ全体を静的にする
export const dynamic = 'force-static';
export default function Page() {
return <div>このページは常に静的</div>;
}
// エラーが発生したら動的にフォールバック
export const dynamic = 'error';
export default function Page() {
return <div>静的を試みて、エラーなら動的</div>;
}
// 再検証時間を設定
export const revalidate = 3600; // 1時間
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{data.title}</div>;
}
ビルド時の出力例
npm run build
# 出力例:
Route (app) Size First Load JS
┌ ○ / 5.2 kB 85 kB
├ ○ /about 1.5 kB 82 kB
├ ○ /blog/[slug] 8.5 kB 90 kB
├ ƒ /dashboard 3.2 kB 85 kB
└ ○ /products/[id] 4.8 kB 87 kB
○ (Static) 静的に生成されるページ
ƒ (Dynamic) 動的にレンダリングされるページ
5. Router Cache(ルーターキャッシュ)
概念
クライアント側で訪問済みのルートをメモリ内にキャッシュし、高速なナビゲーションを実現します。
動作の流れ
ユーザーの行動
↓
/products にアクセス
↓
データ取得・レンダリング
↓
Router Cacheに保存(ブラウザのメモリ内)
↓
/products/123 にアクセス
↓
/products に戻る(<Link>で)
↓
Router Cacheから即座に表示!(超高速⚡)
キャッシュの持続時間
【静的ルート】
- 期間: 5分間
- 自動更新: なし
【動的ルート】
- 期間: 30秒間
- 自動更新: あり(バックグラウンド)
プリフェッチング
Next.jsは、画面に表示されている<Link>のルートを自動的にプリフェッチします。
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
{/* このリンクが画面に表示されると、
/productsのデータが自動的にプリフェッチされる */}
<Link href="/products">
商品一覧
</Link>
{/* ユーザーがクリックすると即座に表示!⚡ */}
</nav>
);
}
プリフェッチの制御
// プリフェッチを無効化
<Link href="/products" prefetch={false}>
商品一覧
</Link>
// 完全なデータをプリフェッチ(デフォルトは部分的)
<Link href="/products" prefetch={true}>
商品一覧
</Link>
Router Cacheの無効化
'use client';
import { useRouter } from 'next/navigation';
export default function RefreshButton() {
const router = useRouter();
return (
<button onClick={() => router.refresh()}>
最新データを取得
</button>
);
}
6. 実践的なキャッシング戦略
パターン1: ニュースサイト
// 記事一覧(5分ごとに更新)
export const revalidate = 300;
export default async function NewsPage() {
const articles = await fetch('https://api.news.com/articles', {
next: { tags: ['news'] }
}).then(r => r.json());
return (
<div>
{articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
);
}
// 新しい記事が投稿されたとき即座に更新
// app/api/webhook/route.js
import { revalidateTag } from 'next/cache';
export async function POST(request) {
const { article } = await request.json();
await createArticle(article);
// 即座にキャッシュ更新
revalidateTag('news');
return Response.json({ success: true });
}
パターン2: ECサイト
// 商品一覧(静的、管理画面から更新)
export default async function ProductsPage() {
const products = await fetch('https://api.shop.com/products', {
next: { tags: ['products'] }
}).then(r => r.json());
return (
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// 商品詳細(基本情報は静的、在庫は動的)
import { Suspense } from 'react';
export default async function ProductPage({ params }) {
// 基本情報は静的キャッシュ
const product = await fetch(`https://api.shop.com/products/${params.id}`, {
next: { tags: ['product'] }
}).then(r => r.json());
return (
<div>
<ProductInfo product={product} />
{/* 在庫情報はキャッシュしない */}
<Suspense fallback={<StockSkeleton />}>
<StockStatus productId={params.id} />
</Suspense>
</div>
);
}
async function StockStatus({ productId }) {
const stock = await fetch(`https://api.shop.com/stock/${productId}`, {
cache: 'no-store' // 毎回最新の在庫を取得
}).then(r => r.json());
return (
<div className={stock.available ? 'in-stock' : 'out-of-stock'}>
{stock.available ? `在庫あり(${stock.quantity}個)` : '在庫切れ'}
</div>
);
}
パターン3: ダッシュボード
// ユーザーごとに異なる→完全に動的
export const dynamic = 'force-dynamic';
import { cookies } from 'next/headers';
export default async function DashboardPage() {
const cookieStore = cookies();
const userId = cookieStore.get('user-id')?.value;
// ユーザー固有のデータ(キャッシュしない)
const userData = await fetch(`https://api.example.com/users/${userId}`, {
cache: 'no-store'
}).then(r => r.json());
return (
<div>
<h1>ようこそ、{userData.name}さん</h1>
<Stats data={userData.stats} />
<RecentActivity activities={userData.activities} />
</div>
);
}
パターン4: 多層キャッシング戦略
// トップページ:複数のキャッシング戦略を組み合わせ
export default async function HomePage() {
return (
<div>
{/* 1. 完全静的(Full Route Cache) */}
<Hero />
<Features />
{/* 2. ISR(Data Cache + 時間ベース) */}
<Suspense fallback={<NewsSkeleton />}>
<LatestNews />
</Suspense>
{/* 3. タグベースISR(Data Cache + オンデマンド) */}
<Suspense fallback={<ProductsSkeleton />}>
<FeaturedProducts />
</Suspense>
{/* 4. キャッシュなし(常に最新) */}
<Suspense fallback={<StatsSkeleton />}>
<LiveStats />
</Suspense>
</div>
);
}
// 完全静的
function Hero() {
return <section>ヒーローセクション</section>;
}
// ISR(5分ごとに更新)
async function LatestNews() {
const news = await fetch('https://api.news.com/latest', {
next: { revalidate: 300 }
}).then(r => r.json());
return (
<section>
{news.map(item => (
<NewsCard key={item.id} news={item} />
))}
</section>
);
}
// タグベースISR
async function FeaturedProducts() {
const products = await fetch('https://api.shop.com/featured', {
next: { tags: ['featured-products'] }
}).then(r => r.json());
return (
<section>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</section>
);
}
// キャッシュなし
async function LiveStats() {
const stats = await fetch('https://api.example.com/stats', {
cache: 'no-store'
}).then(r => r.json());
return (
<section>
<div>訪問者数: {stats.visitors}</div>
<div>オンライン: {stats.online}</div>
</section>
);
}
7. キャッシュデバッグ
開発時のキャッシュ確認
# ビルド時のキャッシュ情報を表示
npm run build
# 出力例:
Route (app) Size First Load JS
┌ ○ / 5.2 kB 85 kB
├ ○ /about 1.5 kB 82 kB
├ ● /blog/[slug] 8.5 kB 90 kB
├ ƒ /dashboard 3.2 kB 85 kB
└ ○ /products/[id] 4.8 kB 87 kB
○ (Static) 静的に生成
● (SSG) 静的に生成(getStaticProps使用)
ƒ (Dynamic) 動的にレンダリング
ログによる確認
export default async function Page() {
console.log('このログがリクエストごとに出る?');
// → 出る: 動的レンダリング(キャッシュされていない)
// → 出ない: 静的レンダリング(キャッシュされている)
const data = await fetch('https://api.example.com/data');
return <div>{data.title}</div>;
}
Next.js 15の開発者ツール
// next.config.js
module.exports = {
logging: {
fetches: {
fullUrl: true // fetchのログを詳細表示
}
}
}
ログ出力例:
GET https://api.example.com/data 200 in 245ms (cache: HIT)
GET https://api.example.com/user 200 in 180ms (cache: MISS)
GET https://api.example.com/posts 200 in 120ms (cache: SKIP)
キャッシュの手動クリア
# 開発環境でキャッシュをクリア
rm -rf .next
# 本番環境では再デプロイが必要
デバッグ用のヘルパー関数
// lib/cache-debug.js
export function logCacheStatus(label, isCached) {
if (process.env.NODE_ENV === 'development') {
console.log(`[Cache Debug] ${label}: ${isCached ? '✅ HIT' : '❌ MISS'}`);
}
}
// 使用例
export default async function Page() {
const startTime = Date.now();
const data = await fetch('https://api.example.com/data');
const duration = Date.now() - startTime;
logCacheStatus('Products', duration < 10); // 10ms以下ならキャッシュヒット
return <div>{data.title}</div>;
}
8. キャッシング戦略のベストプラクティス
1. 適切なキャッシュ期間の設定
// 変更頻度に応じて設定
const CACHE_TIMES = {
STATIC: Infinity, // 変わらない(会社概要など)
DAILY: 86400, // 1日(ブログ記事など)
HOURLY: 3600, // 1時間(ニュースなど)
FREQUENT: 300, // 5分(商品在庫など)
REALTIME: 0 // リアルタイム(株価など)
};
// 使用例
fetch(url, { next: { revalidate: CACHE_TIMES.HOURLY } });
2. タグの命名規則
// 良い例:階層的でわかりやすい
tags: ['products', 'product-123', 'category-electronics']
// 悪い例:あいまいで管理しづらい
tags: ['data', 'item', 'stuff']
3. キャッシュとリアルタイム性のバランス
export default async function ProductPage({ params }) {
// 基本情報:キャッシュ(あまり変わらない)
const product = await fetch(`/api/products/${params.id}`, {
next: { revalidate: 3600, tags: ['product'] }
});
// 価格・在庫:リアルタイム(頻繁に変わる)
return (
<div>
<ProductInfo product={product} />
<Suspense fallback={<PriceSkeleton />}>
<RealTimePrice productId={params.id} />
</Suspense>
</div>
);
}
4. キャッシュの監視
// app/api/cache-stats/route.js
export async function GET() {
const stats = {
hitRate: calculateCacheHitRate(),
missRate: calculateCacheMissRate(),
avgResponseTime: calculateAvgResponseTime()
};
return Response.json(stats);
}
まとめ
この記事では、Next.jsの4層のキャッシング戦略を詳しく学びました:
- ✅ Request Memoization: 同一リクエスト内での重複排除
- ✅ Data Cache: 永続的なデータキャッシュと再検証
- ✅ Full Route Cache: 静的ルート全体のキャッシュ
- ✅ Router Cache: クライアント側のナビゲーションキャッシュ
- ✅ Next.js 14と15のキャッシング動作の違い
- ✅ 実践的なキャッシング戦略とデバッグ方法
次回は「エラーハンドリング・メタデータ・実践パターン」について解説します。シリーズ最終回として、エラー処理、SEO最適化、そして実践的な運用ノウハウをまとめます。
お楽しみに!
Discussion