🎉

【Next.js完全ガイド Vol.4】4層のキャッシング戦略を完全マスター

に公開

【Next.js完全ガイド Vol.4】4層のキャッシング戦略を完全マスター

はじめに

この記事は「Next.js完全ガイド」シリーズの第4回です。

📚 シリーズ構成

前回のおさらい

前回は、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