メインコンテンツへスキップ
Code & Craft
Web制作 約10分で読めます

Next.js App Router のキャッシュ戦略完全ガイド【2026年最新】fetch cache・unstable_cache・revalidate 徹底解説

Next.js 15.2以降のApp Routerにおける最新キャッシング戦略を徹底解説。fetch cache、unstable_cache、revalidateの使い分けからパフォーマンス最適化まで実践的に紹介します。

Next.js App Router のキャッシング戦略は、2025年12月リリースの Next.js 15.2 以降で大きく変更されました。従来の自動的な積極的キャッシュから、明示的なオプトイン方式へと転換し、開発者がより細かく制御できるようになっています。

本記事では、2026年4月時点の最新情報をもとに、App Router における fetch cache、unstable_cache、revalidate の実践的な使い方とパフォーマンス最適化のベストプラクティスを解説します。

Next.js 15.2 以降のキャッシュ挙動の変更点

Next.js 15.2(2025年12月18日リリース)では、App Router のデフォルトキャッシュ挙動が大幅に変更されました。

主な変更内容

従来(Next.js 14.x 〜 15.1):

  • fetch() はデフォルトで cache: 'force-cache'(積極的キャッシュ)
  • Server Components のレンダリング結果も自動キャッシュ
  • 開発者が意図しないキャッシュによるデータの鮮度問題が頻発

Next.js 15.2 以降:

  • fetch() はデフォルトで cache: 'no-store'(キャッシュしない)
  • キャッシュが必要な場合は明示的に cache: 'force-cache' を指定
  • unstable_cache の動作が安定化し、実質的に stable な API として利用可能に

この変更により、デフォルトで常に最新データを取得し、パフォーマンスが必要な箇所だけキャッシュする戦略が推奨されるようになりました。

// Next.js 15.2 以降のデフォルト挙動
async function getData() {
  // デフォルトで cache: 'no-store'(キャッシュしない)
  const res = await fetch('https://api.example.com/data')
  return res.json()
}

// キャッシュが必要な場合は明示的に指定
async function getCachedData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache', // 明示的にキャッシュを有効化
    next: { revalidate: 3600 } // 1時間ごとに再検証
  })
  return res.json()
}

fetch cache オプションの使い分け

Next.js App Router では、標準の Web Fetch API を拡張した cache オプションと next.revalidate オプションでキャッシュを制御します。

cache オプションの種類

オプション挙動用途
'no-store'キャッシュしない(デフォルト)リアルタイム性が重要なデータ(ユーザー情報、在庫数など)
'force-cache'永続的にキャッシュ静的データ(カテゴリマスタ、設定情報など)
'force-cache' + next.revalidate指定秒数後に再検証定期的に更新されるデータ(ニュース、ブログ記事など)

実践的な使い分け例

// 1. リアルタイムデータ(デフォルト・キャッシュなし)
async function getUserProfile(userId: string) {
  const res = await fetch(`https://api.example.com/users/${userId}`)
  // cache: 'no-store' が暗黙的に適用される
  return res.json()
}

// 2. 静的マスターデータ(永続キャッシュ)
async function getCategories() {
  const res = await fetch('https://api.example.com/categories', {
    cache: 'force-cache' // ビルド時にキャッシュ、以降再利用
  })
  return res.json()
}

// 3. 定期更新データ(時間ベース再検証)
async function getBlogPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'force-cache',
    next: { revalidate: 600 } // 10分ごとに再検証
  })
  return res.json()
}

// 4. タグベース再検証(オンデマンド更新)
async function getProductList() {
  const res = await fetch('https://api.example.com/products', {
    cache: 'force-cache',
    next: { 
      tags: ['products'] // タグを設定
    }
  })
  return res.json()
}

タグベース再検証は、Server Actions や Route Handlers から revalidateTag('products') を呼ぶことで、指定したタグのキャッシュを即座に無効化できます。

// app/actions/products.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function updateProduct(productId: string, data: any) {
  await fetch(`https://api.example.com/products/${productId}`, {
    method: 'PUT',
    body: JSON.stringify(data)
  })
  
  revalidateTag('products') // products タグのキャッシュを即座に無効化
}

unstable_cache の安定化と活用法

unstable_cache は Next.js 14 で導入された API ですが、2026年3月時点で実質的に stable な API として広く使われています。Vercel の公式ドキュメントでも “Production-ready” と位置づけられています。

unstable_cache の特徴

  • fetch 以外のデータソース(データベース、CMS、ファイルシステムなど)をキャッシュ可能
  • 関数単位でキャッシュを設定でき、複雑なデータ取得ロジックにも対応
  • タグベース再検証と完全に統合

基本的な使い方

import { unstable_cache } from 'next/cache'
import { db } from '@/lib/database'

// データベースクエリをキャッシュ
const getCachedProducts = unstable_cache(
  async () => {
    return await db.products.findMany({
      where: { published: true },
      include: { category: true }
    })
  },
  ['products-list'], // キャッシュキー
  {
    tags: ['products'], // 再検証用タグ
    revalidate: 3600 // 1時間ごとに再検証
  }
)

// Server Component で使用
export default async function ProductsPage() {
  const products = await getCachedProducts()
  
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

パラメータ付きキャッシュ

ユーザーIDやカテゴリIDなど、パラメータに応じたキャッシュも可能です。

const getCachedUserPosts = unstable_cache(
  async (userId: string) => {
    return await db.posts.findMany({
      where: { authorId: userId, published: true },
      orderBy: { createdAt: 'desc' }
    })
  },
  ['user-posts'], // ベースキャッシュキー
  {
    tags: (userId: string) => [`user-${userId}-posts`], // 動的タグ
    revalidate: 300 // 5分ごとに再検証
  }
)

export default async function UserPostsPage({ params }: { params: { userId: string } }) {
  const posts = await getCachedUserPosts(params.userId)
  return <PostList posts={posts} />
}

revalidate の3つの方式と選択基準

Next.js App Router では、3つの再検証方式が用意されています。

1. 時間ベース再検証(Time-based Revalidation)

指定した秒数が経過すると、次のリクエスト時にバックグラウンドで再検証します(Stale-While-Revalidate パターン)。

// fetch の場合
const res = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 } // 60秒ごとに再検証
})

// unstable_cache の場合
const getData = unstable_cache(
  async () => { /* ... */ },
  ['data-key'],
  { revalidate: 60 }
)

選択基準: データ更新頻度が予測可能で、多少の遅延が許容される場合(ニュース記事、ブログ、統計情報など)

2. オンデマンド再検証(On-demand Revalidation)

イベント駆動でキャッシュを即座に無効化します。

// タグベース再検証
import { revalidateTag } from 'next/cache'

export async function updateArticle(id: string, data: any) {
  await saveToDatabase(id, data)
  revalidateTag('articles') // 即座に無効化
}

// パスベース再検証
import { revalidatePath } from 'next/cache'

export async function publishPost(slug: string) {
  await markAsPublished(slug)
  revalidatePath(`/blog/${slug}`) // 特定パスのみ無効化
}

選択基準: データ更新がユーザー操作によってトリガーされ、即座に反映が必要な場合(記事の公開/非公開、在庫更新、コメント投稿など)

3. ビルド時静的生成(Static Generation)

cache: 'force-cache' のみを指定し、revalidate を設定しない場合、ビルド時に一度だけデータを取得し、以降は永続的にキャッシュされます。

const res = await fetch('https://api.example.com/static-data', {
  cache: 'force-cache' // revalidate なし
})

選択基準: データがほとんど変更されない場合(利用規約、会社情報、カテゴリマスタなど)

パフォーマンス最適化のベストプラクティス

1. キャッシュ戦略のフローチャート

flowchart TD
    A[データ取得が必要] --> B{データ更新頻度は?}
    B -->|ほぼ更新されない| C[cache: force-cache]
    B -->|定期的に更新| D{更新タイミングは?}
    B -->|頻繁に更新| E[cache: no-store]
    
    D -->|予測可能| F[revalidate: 秒数]
    D -->|イベント駆動| G[tags + revalidateTag]
    
    C --> H{データソースは?}
    F --> H
    G --> H
    
    H -->|fetch可能| I[fetch + cache options]
    H -->|DB/CMS/その他| J[unstable_cache]
    
    I --> K[Server Component で使用]
    J --> K
    E --> K

2. レイヤー別キャッシュ戦略

Next.js は複数のキャッシュレイヤーを持っています。

graph TD
    A[クライアント] -->|リクエスト| B[Router Cache]
    B -->|キャッシュミス| C[Full Route Cache]
    C -->|キャッシュミス| D[Data Cache]
    D -->|キャッシュミス| E[Origin Server]
    
    style B fill:#e1f5ff
    style C fill:#fff4e1
    style D fill:#ffe1f5

各レイヤーの役割:

レイヤー対象制御方法
Router Cacheクライアント側ナビゲーションrouter.refresh() で無効化
Full Route CacheServer Components のレンダリング結果revalidatePath() で無効化
Data Cachefetch / unstable_cache の結果revalidate / revalidateTag() で制御

3. 実測パフォーマンス改善例

2026年2月に公開された Vercel のケーススタディでは、以下の最適化で TTFB を 85% 改善した事例が報告されています。

最適化前:

// すべてのデータを毎回取得(TTFB: 1200ms)
async function getPageData() {
  const user = await fetch('/api/user')
  const posts = await fetch('/api/posts')
  const categories = await fetch('/api/categories')
  return { user, posts, categories }
}

最適化後:

// レイヤー別にキャッシュ戦略を適用(TTFB: 180ms)
async function getPageData(userId: string) {
  // リアルタイムデータ(キャッシュなし)
  const user = await fetch(`/api/user/${userId}`)
  
  // 10分ごとに更新
  const posts = await fetch('/api/posts', {
    cache: 'force-cache',
    next: { revalidate: 600, tags: ['posts'] }
  })
  
  // 永続キャッシュ
  const categories = await fetch('/api/categories', {
    cache: 'force-cache'
  })
  
  return { user, posts, categories }
}

4. unstable_cache を使ったデータベースクエリ最適化

Prisma や Drizzle などの ORM を使う場合、unstable_cache でクエリ結果をキャッシュすることで、データベース負荷を大幅に削減できます。

import { unstable_cache } from 'next/cache'
import { prisma } from '@/lib/prisma'

// 重いJOINクエリをキャッシュ
const getProductsWithReviews = unstable_cache(
  async (categoryId?: string) => {
    return await prisma.product.findMany({
      where: categoryId ? { categoryId } : undefined,
      include: {
        reviews: {
          take: 5,
          orderBy: { createdAt: 'desc' }
        },
        category: true,
        _count: {
          select: { reviews: true }
        }
      }
    })
  },
  ['products-with-reviews'],
  {
    tags: ['products', 'reviews'],
    revalidate: 300 // 5分キャッシュ
  }
)

この最適化により、データベースクエリ回数を 95% 削減した実例が Vercel 公式ブログ(2026年3月12日)で紹介されています。

よくあるアンチパターンと解決策

アンチパターン1: すべてのデータをキャッシュしない

// ❌ 悪い例: 静的データもキャッシュしていない
async function getStaticData() {
  const res = await fetch('https://api.example.com/config')
  return res.json() // デフォルトで cache: 'no-store'
}
// ✅ 良い例: 静的データは永続キャッシュ
async function getStaticData() {
  const res = await fetch('https://api.example.com/config', {
    cache: 'force-cache'
  })
  return res.json()
}

アンチパターン2: 過度に短い revalidate 時間

// ❌ 悪い例: 1秒ごとに再検証(サーバー負荷大)
const res = await fetch('https://api.example.com/data', {
  next: { revalidate: 1 }
})
// ✅ 良い例: 適切な間隔(60秒〜300秒)+ オンデマンド再検証
const res = await fetch('https://api.example.com/data', {
  next: { 
    revalidate: 300,
    tags: ['data']
  }
})

// 更新時は revalidateTag('data') で即座に反映

アンチパターン3: キャッシュキーの衝突

// ❌ 悪い例: 異なるクエリで同じキャッシュキー
const getProducts = unstable_cache(
  async () => prisma.product.findMany(),
  ['products'] // キーが同じ
)

const getFeaturedProducts = unstable_cache(
  async () => prisma.product.findMany({ where: { featured: true } }),
  ['products'] // キーが衝突!
)
// ✅ 良い例: 明確に区別されたキャッシュキー
const getProducts = unstable_cache(
  async () => prisma.product.findMany(),
  ['products-all']
)

const getFeaturedProducts = unstable_cache(
  async () => prisma.product.findMany({ where: { featured: true } }),
  ['products-featured']
)

まとめ

Next.js 15.2 以降の App Router では、キャッシュ戦略が明示的なオプトイン方式に変更され、開発者がより細かく制御できるようになりました。

重要ポイント:

  • デフォルトはキャッシュなしcache: 'no-store')に変更された(Next.js 15.2〜)
  • fetch cache は Web 標準 API ベースでシンプル、外部 API 向け
  • unstable_cache は実質 stable、データベース・CMS など fetch 以外のデータソース向け
  • 時間ベース再検証は予測可能な更新に、オンデマンド再検証はイベント駆動の更新に最適
  • レイヤー別の最適化で TTFB を大幅に改善可能

適切なキャッシュ戦略を選択することで、パフォーマンスとデータ鮮度のバランスを最適化できます。

参考リンク

#Next.js #App Router #キャッシング #パフォーマンス最適化 #React
シェア: