言語を切り替える
テーマを切り替える

Next.js キャッシュメカニズム完全ガイド:revalidate の正しい使用タイミングを習得する

画面上の「古いデータ」を、もう20回もリロードしています。10分前にデータベースのタイトルを直接書き換えたにもかかわらず、ページはいっこうに更新されません。コードには revalidate: 60 と明記されています。それを revalidate: 10 に変更し、サーバーを再起動して再読み込みしても、表示は古いままです。

Next.js のキャッシュ機能は、フレームワークの中でも特に複雑で開発者を悩ませる部分です。4層のキャッシュ構造、3つの revalidate メソッド、そしてバージョン 14 から 15 への破壊的変更。revalidate: 60 を設定するだけでは解決しないことも多く、Router Cache や Full Route Cache の挙動、あるいは開発環境特有の仕様が影響している場合があります。

本記事では、以下のポイントをわかりやすく解説します。

  • 4層のキャッシュがそれぞれ何を制御しているのか
  • revalidatePath、revalidateTag、updateTag の違いと適切な使い分け
  • データが更新されない時のステップ順のトラブルシューティング

再検証が動かない、どの API を選ぶべきか迷うといった問題に直面しているなら、次の 12 分が、徹夜の数回分を節約してくれるかもしれません。

なぜ Next.js のキャッシュはこんなに複雑なのか

なぜこれほど多くの層が必要なのか?

正直に言って、初めて Next.js に4層のキャッシュがあると知った時、私も今のあなたと同じような顔をしていました。Request Memoization? Full Route Cache? 何それ? もっとシンプルにできないの?

しかし冷静に考えると、各層のキャッシュはそれぞれ異なる環境下のパフォーマンス問題を解決しています:

  • コンポーネントツリー内で10個のコンポーネントがユーザー情報を取得する場合、リクエストを10回飛ばすわけにはいきませんよね?
  • ブログ記事一覧が1日に数回しか更新されないなら、アクセスのたびに再レンダリングするのは無駄ですよね?
  • ユーザーが「戻る」ボタンを押した時、ページ再読み込みを待たせるわけにはいきませんよね?

各層にはそれぞれの役割があります。問題は、それらが相互に影響し合うことです。データを変更したのに、どのキャッシュをクリアすればいいのか分からないのはそのためです。

Next.js 14 vs 15:キャッシュ革命

2024年末、Next.js 15 は大きなニュースを発表しました:デフォルトで fetch リクエストをキャッシュしなくなりました。

以前(14):

fetch(url) // デフォルトでキャッシュ, cache: 'force-cache' と同等

現在(15):

fetch(url) // デフォルトでキャッシュしない, cache: 'no-store' と同等

この変更により、アップグレード後にパフォーマンスが急激に低下したと感じる人が続出しました。元々自動的にキャッシュされていたデータがキャッシュされなくなったからです。Vercel のフォーラムは大荒れでしたが、公式の理由は「明示的であることは暗黙的であるより良い。キャッシュは開発者が能動的に選択すべきもので、デフォルトの挙動であるべきではない」というものでした。

理屈は通っていますが、既存プロジェクトにとっては破壊的な変更です。

4層キャッシュのパノラマ

簡単に言えば、データがサーバーからユーザーのブラウザに届くまで、以下の4層を経由します:

  1. Request Memoization(リクエスト記憶化)
    スコープ:単一リクエストのレンダリングサイクル
    管理者:React

  2. Data Cache(データキャッシュ)
    スコープ:サーバー、リクエスト間で永続化
    管理者:Next.js

  3. Full Route Cache(完全ルートキャッシュ)
    スコープ:サーバー、静的ルート
    管理者:Next.js

  4. Router Cache(ルーターキャッシュ)
    スコープ:クライアントブラウザのメモリ
    管理者:Next.js

データの流れはおおよそ以下のようになります:

ユーザーアクセス → Router Cache(クライアント)→ Full Route Cache(サーバー)

                                  Data Cache → Request Memoization → データソース

あなたの revalidate は主に Data Cache と Full Route Cache に影響します。一方、Router Cache をクリアするには router.refresh() かハードリロードが必要です。

これが、revalidate したはずなのにクライアントのリロードで古いデータが表示される理由です。Router Cache が残っているからです。

次章では、これら4つのキャッシュを一層ずつ分解し、それぞれが何をしているのか、いつ有効になり、いつ無効になるのかを解説します。

4層キャッシュメカニズム詳細

Request Memoization(リクエスト記憶化)

これは何?

これは実は React 18 の機能で、Next.js 独自のものではありません。簡単に言うと、1回のレンダリングサイクル内で複数のコンポーネントが同じ GET リクエストを送信した場合、React が自動的にそれらを1回のリクエストに統合します。

例:

// app/page.tsx
async function UserProfile() {
  const user = await fetch('https://api.example.com/user/123')
  return <div>{user.name}</div>
}

async function UserAvatar() {
  const user = await fetch('https://api.example.com/user/123')  // 同じリクエスト
  return <img src={user.avatar} />
}

export default function Page() {
  return (
    <>
      <UserProfile />
      <UserAvatar />
    </>
  )
}

これら2つのコンポーネントは同じ URL をリクエストしていますが、実際には1回しかリクエストが飛びません。React は最初の結果を記憶し、2回目はキャッシュを使用します。

超短命なライフサイクル

このキャッシュは単一のレンダリングプロセス中のみ有効です。レンダリングが終わればキャッシュは消えます。次にユーザーがページをリロードすれば、また新しいリクエストになります。

注意点

  • サーバーコンポーネント(Server Component)でのみ有効
  • GET リクエストでのみ有効(POST, PUT 等は記憶されない)
  • 開発環境では効果が見えない場合があります(コード変更のたびに再レンダリングされるため)

いつ使う?

おそらく意識する必要はありません。これは React が自動的に行う最適化で、手動制御はできません。言及したのは、「複数のコンポーネントで同じ fetch を書いても重複リクエストの心配はない」と伝えるためです。

Data Cache(データキャッシュ)

これこそが重要

Data Cache は Next.js キャッシュメカニズムの中核です。fetch リクエストの結果をサーバーのファイルシステムに保存し、リクエスト間、ユーザー間、デプロイ間で永続化させます。

Next.js 14 vs 15 の雲泥の差

Next.js 14:

fetch('https://api.example.com/posts')
// 以下と同等
fetch('https://api.example.com/posts', { cache: 'force-cache' })
// 結果:手動で revalidate しない限り、データは永久にキャッシュされる

Next.js 15:

fetch('https://api.example.com/posts')
// 以下と同等
fetch('https://api.example.com/posts', { cache: 'no-store' })
// 結果:毎回再リクエスト、キャッシュしない

データをキャッシュしたい場合(Next.js 15)

方法1:単一リクエストで設定

fetch('https://api.example.com/posts', {
  cache: 'force-cache',
  next: { revalidate: 3600 }  // 1時間後に再検証
})

方法2:ルート全体で設定

// app/blog/page.tsx
export const revalidate = 3600

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  // ...
}

いつ無効になる?

Data Cache が無効になるタイミングは3つ:

  1. 設定した revalidate 時間が経過した(例:3600秒)
  2. 手動で revalidatePath() または revalidateTag() を呼び出した
  3. アプリを再デプロイした(Data Cache の設定によるが、通常はクリアされる場合が多い)

複数の fetch で revalidate 時間が違う場合は?

1つのページに複数の fetch があり、それぞれ revalidate 時間が異なる場合、Next.js は最も短い時間をページ全体の再検証時間として採用します。

async function Page() {
  const posts = await fetch('...', { next: { revalidate: 60 } })    // 60秒
  const user = await fetch('...', { next: { revalidate: 3600 } })  // 1時間

  // 実際には、ページ全体が60秒ごとに再検証される
}

Full Route Cache(完全ルートキャッシュ)

HTML レベルのキャッシュ

Data Cache がデータをキャッシュするなら、Full Route Cache はページ全体の HTML と RSC Payload(React Server Component のシリアライズデータ)をキャッシュします。

いつキャッシュされる?

静的レンダリングされるルートのみキャッシュされます。静的レンダリングとは? ビルド時に内容を確定できるページのことです。

逆に、ページで以下を使用すると動的レンダリングとなり、キャッシュされません:

  • cookies()
  • headers()
  • searchParams
  • 不安定な関数(Math.random()Date.now() など)

ページが静的か動的かの判断方法

npm run build を実行すると、ターミナルに表示されます:

Route (app)                              Size     First Load JS
┌ ○ /                                    5 kB           87 kB
├ ● /blog                                1 kB           88 kB
└ ƒ /api/user                            0 kB           87 kB

○  (Static)  静的HTMLとして自動レンダリング(動的データなし)
●  (SSG)     静的HTML + JSONとして自動生成(getStaticProps使用)
ƒ  (Dynamic) サーバーサイドでオンデマンドレンダリング

なら静的でキャッシュされます。ƒ なら動的でキャッシュされません。

静的・動的の強制

// 静的を強制
export const dynamic = 'force-static'

// 動的を強制
export const dynamic = 'force-dynamic'

いつ無効になる?

Full Route Cache の無効化タイミング:

  1. Data Cache が無効になった時(データが変わればページも再レンダリングが必要だから)
  2. revalidatePath('/blog') を呼び出した時
  3. アプリを再デプロイした時

Router Cache(ルーターキャッシュ)

クライアント側の小細工

Router Cache はユーザーのブラウザメモリ内に存在するキャッシュです。ユーザーがページを訪問すると、Next.js はそのページの内容をクライアント側にキャッシュします。次回ユーザーが「戻る」ボタンを押したり、そのページに移動したりすると、サーバーにリクエストせずキャッシュを使用します。

プリフェッチという裏技

<Link href="/about"> を使用すると、リンクがユーザーの視界に入った時点で、Next.js は /about ページの内容を自動的にプリフェッチし、Router Cache に保存します。ユーザーが実際にクリックした瞬間、即座に遷移できます。

ライフサイクル(Next.js 14)

  • 静的ルート:5分間キャッシュ
  • 動的ルート:30秒間キャッシュ

Next.js 15 での変更点

Next.js 15 はデフォルトで Router Cache を有効にしません(あるいはキャッシュ時間が非常に短いです)。有効にしたい場合は next.config.js で設定が必要です:

// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 30,      // 動的ルート30秒
      static: 180,      // 静的ルート180秒
    },
  },
}

いつ無効になる?

  • キャッシュ時間が経過した
  • ユーザーがハードリロード(Ctrl+Shift+R)した
  • router.refresh() を呼び出した

なぜ revalidate が効かないように見えるのか?

多くの場合、サーバー側で revalidatePath を呼び出してデータは更新されているのに、ユーザーがリロードしても古いデータが見えるのは、Router Cache がまだ有効だからです。

解決策:

  1. ハードリロード(ユーザーに強要はできませんが)
  2. データ更新後に router.refresh() を呼び出す(クライアントコンポーネントの場合)
  3. Router Cache の時間を短くする

revalidate メソッド全解析

さて、4層キャッシュの原理は分かりました。ここからは最も実用的な部分、どうやってキャッシュを無効にするかです。

Next.js はいくつかの revalidate メソッドを提供しており、それぞれ適用シーンが異なります。違いを理解すれば、デバッグ時間を大幅に節約できます。

時間ベースの revalidate(Time-based)

最も一般的な方法

時間ベースの revalidate は、「X秒ごとにデータを自動再取得する」というものです。これが ISR(Incremental Static Regeneration、増分静的再生成)の核心メカニズムです。

2つの書き方

方法1:fetch リクエスト内で設定

const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }  // 3600秒 = 1時間
})

方法2:ルートファイルのトップレベルで設定

// app/blog/page.tsx
export const revalidate = 3600

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  return <PostList posts={posts} />
}

動作原理(ISR)

revalidate: 3600 を設定したと仮定します:

  1. 最初のユーザーがページ訪問 → 静的 HTML 生成、1時間キャッシュ
  2. 続く1時間、全ユーザーはそのキャッシュされた HTML を見る(超高速)
  3. 1時間後、次のユーザーが訪問 → まだ古い HTML を返す(待たせない)
  4. しかし同時に、Next.js はバックグラウンドで新しい HTML を再生成する
  5. 新 HTML 生成完了後、それ以降のユーザーは新しいコンテンツを見る

このメカニズムを stale-while-revalidate(再検証中は古いコンテンツを返す)と呼びます。メリットはユーザーを待たせないこと、デメリットは常に1人のユーザーが古いデータを見ることになる点です。

適用シーン

  • ブログ記事一覧(1時間に数回更新)
  • ニューストップページ(30分ごとに更新)
  • 商品カタログ(1日1回更新)

よくある問題1:開発環境で効かない

revalidate 設定したのに効かない」という不満をよく聞きます。最初に確認すべきは、開発環境と本番環境どちらでテストしているか? です。

開発環境(npm run dev)では、Next.js はほとんどのキャッシュを無効にし、リクエストごとに再レンダリングします。本番環境でテストする必要があります:

npm run build
npm start

よくある問題2:複数の fetch の時間が不一致

前述の通り、ページ内に複数の fetch があり revalidate 時間が異なる場合、Next.js は最小値を取ります。しかし詳細があります:Data Cache は各 fetch 独自の時間に従います

async function Page() {
  // このリクエストは60秒ごとに再検証
  const posts = await fetch('...', { next: { revalidate: 60 } })

  // このリクエストは3600秒ごとに再検証
  const user = await fetch('...', { next: { revalidate: 3600 } })
}

ページは60秒ごとに再レンダリングされますが、user リクエストのデータキャッシュは1時間保持されます。つまり、最初の1時間は60秒ごとにページが再構築されても user データは変わらず、1時間後にようやく user データが更新されます。

ややこしいですが、合理的設計です。

オンデマンド revalidate:revalidatePath

ユーザー主導の更新

時間ベースがタイマーなら、revalidatePath はスイッチです。特定のイベント(例:ユーザーが記事を投稿)が発生した時、手動でキャッシュを無効化します。

使用方法

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function publishPost(formData) {
  // 記事投稿ロジック
  await db.posts.create({ ... })

  // ブログ一覧ページのキャッシュを無効化
  revalidatePath('/blog')
}

クライアントコンポーネントで呼び出し:

// app/components/PublishButton.tsx
'use client'

import { publishPost } from '@/app/actions'

export function PublishButton() {
  return (
    <form action={publishPost}>
      <button type="submit">記事を公開</button>
    </form>
  )
}

パスタイプの指定

revalidatePath はパスの種類を指定できます:

// /blog このページだけ再検証
revalidatePath('/blog', 'page')

// /blog 以下の全ページ(/blog/post-1, /blog/post-2 含む)を再検証
revalidatePath('/blog', 'layout')

重要:即時再生成ではない

多くの人が revalidatePath を呼べば即座にページが再生成されると思っていますが、違います

revalidatePath はキャッシュを期限切れとしてマークするだけです。実際の再生成は、次にユーザーがそのページにアクセスした時に発生します。

つまり:

  1. あなたが revalidatePath('/blog') を呼ぶ
  2. キャッシュがクリアされる
  3. 次のユーザーが /blog を訪問 → この時初めて HTML 再生成(ユーザーは待つことになる)
  4. 以降のユーザーは新しいコンテンツを見る

適用シーン

  • ユーザーがコンテンツ投稿後に一覧を更新
  • 管理者が設定変更後に関連ページを更新
  • フォーム送信後に現在のページを更新

オンデマンド revalidate:revalidateTag

より柔軟な無効化制御

revalidatePath がパス単位なら、revalidateTagタグ単位です。データにタグを付け、そのタグが付いた全キャッシュを一括で無効化できます。

使用方法

ステップ1:fetch リクエストにタグ付け

const posts = await fetch('https://api.example.com/posts', {
  next: {
    revalidate: 3600,
    tags: ['posts']  // 'posts' タグを付与
  }
})

const authors = await fetch('https://api.example.com/authors', {
  next: {
    revalidate: 3600,
    tags: ['posts', 'authors']  // 複数タグも可
  }
})

ステップ2:特定タグのキャッシュを無効化

'use server'

import { revalidateTag } from 'next/cache'

export async function publishPost() {
  await db.posts.create({ ... })

  // 'posts' タグが付いた全データが無効化される
  revalidateTag('posts')
}

profile=“max” の stale-while-revalidate 戦略

Next.js 15 では profile="max" の使用が推奨されます:

revalidateTag('posts', { profile: 'max' })

profile="max" 使用時:

  1. 期限切れとマークするが、キャッシュを即時削除しない
  2. 次のアクセス時 → 古いデータを返す(爆速!)
  3. 同時にバックグラウンドで新データを取得
  4. 新データ準備完了後、以降のリクエストは新データを返す

これはデフォルトの挙動より優れています。ユーザーを待たせないからです。

revalidatePath vs revalidateTag の違い

比較項目revalidatePathrevalidateTag
無効化粒度パス単位データタグ単位
ページまたぎ具体的なパス指定が必要複数ページにまたがって可能
精細度粗い細かい
複雑さ簡単タグ設計が必要

いつ Tag を使うべき?

データが複数のページで使われている場合、Tag が適しています。

例えばブログシステム:

  • 記事一覧 /blog
  • 記事詳細 /blog/[slug]
  • 著者ページ /author/[id]
  • トップページの「最新記事」モジュール

これら4ページすべてが「記事データ」を使用しています。revalidatePath ならこうなります:

revalidatePath('/blog')
revalidatePath('/blog/[slug]')
revalidatePath('/author/[id]')
revalidatePath('/')

しかし記事データに posts タグを付けていれば:

revalidateTag('posts')

これだけで済みます。圧倒的に楽です。

新機能:updateTag(Next.js 15)

遅延ではなく、即時無効化

updateTag は Next.js 15 の新 API で、revalidateTag との違いは、キャッシュを即時削除し、期限切れマークではない点です。

'use server'

import { updateTag } from 'next/cache'

export async function updateUserProfile(userId, newData) {
  await db.users.update({ where: { id: userId }, data: newData })

  // ユーザーデータキャッシュを即時無効化
  updateTag(`user-${userId}`)
}

revalidateTag との違い

比較項目revalidateTagupdateTag
無効化方式期限切れマーク、次回アクセス時にバックグラウンド更新キャッシュ即時削除
次回アクセス古いデータを返し、裏で取得待機し、新データを取得
使用制限どこでも使用可Server Actions 内のみ
適用シーン一般的なシーン、速度重視”Read-your-writes”(自分の書き込みを読む)シーン

“Read-your-writes”とは?

ユーザーがプロフィール画面でニックネームを変更し保存ボタンを押した後、ページには即座に新しいニックネームが表示されるべきです。古いのが表示されて(バックグラウンド更新を待つ)は困ります。

こういうシーンで updateTag を使います:

export async function updateProfile(formData) {
  const userId = getCurrentUserId()

  await db.users.update({
    where: { id: userId },
    data: { nickname: formData.get('nickname') }
  })

  // 即時無効化し、次回読み込みで最新データを保証
  updateTag(`user-${userId}`)

  revalidatePath('/profile')
}

新機能:use cache ディレクティブ(Next.js 15)

明示的なキャッシュ宣言

Next.js 15 では新たに 'use cache' ディレクティブが導入され、どの関数をキャッシュすべきか明示的にマークできるようになりました。

使用方法

'use cache'

export async function getPopularPosts() {
  const posts = await db.posts.findMany({
    orderBy: { views: 'desc' },
    take: 10
  })
  return posts
}

cacheTag との併用

import { unstable_cacheTag as cacheTag } from 'next/cache'

'use cache'

export async function getPostsByAuthor(authorId) {
  cacheTag('posts', `author-${authorId}`)

  return await db.posts.findMany({
    where: { authorId }
  })
}

その後 revalidateTag で無効化できます:

revalidateTag(`author-${authorId}`)

なぜこれが必要?

Next.js 15 では fetch がデフォルトでキャッシュされないため、毎回 DB クエリを投げたくない場合、明示的にキャッシュを宣言する必要があります。use cache はその意図を明確にします。

よくある問題トラブルシューティング

理論は以上です。ここからは最も実用的なキャッシュトラブル解決法です。

よくある4つの問題と、その調査手順・解決策をリストアップします。

問題1: revalidate を設定したのに効かない

症状

export const revalidate = 60 を設定したのに、10分後にアクセスしてもデータが古いまま。

調査手順

1. 本番環境でのテストか確認

これが最大の落とし穴です。開発環境(npm run dev)はキャッシュの大部分を無効にします。

必ず以下でテストしてください:

npm run build
npm start

2. ルートが動的レンダリングになっていないか確認

npm run build を実行し、出力を見ます:

Route (app)                Size
├ ○ /blog                  1 kB    ← 静的、キャッシュされる
└ ƒ /profile               2 kB    ← 動的、キャッシュされない

ルートが ƒ(動的)なら、revalidate は機能しません。動的ルートはキャッシュされないからです。

動的レンダリングを引き起こすコード:

// これらを使うと動的レンダリングになる
import { cookies } from 'next/headers'
import { headers } from 'next/headers'

export default function Page({ searchParams }) {  // searchParams 使用
  const cookieStore = cookies()  // cookies 使用
  // ...
}

解決策:

  • 動的レンダリング不要なら、それらを除外
  • 必要なら、revalidate は諦めてオンデマンド revalidate を使う

3. Next.js バージョンの確認

Next.js 14 と 15 はデフォルト挙動が異なります。15 に上げたばかりなら、以前キャッシュされていたものがされなくなっている可能性があります。

解決策(Next.js 15):

// 明示的にキャッシュ有効化
fetch(url, {
  cache: 'force-cache',
  next: { revalidate: 60 }
})

// または use cache ディレクティブ
'use cache'
export async function getData() {
  // ...
}

4. Router Cache の干渉確認

サーバーデータは更新されていても、クライアントの Router Cache が古いデータを持っている可能性があります。

解決策:

  • ハードリロード(Ctrl+Shift+R)
  • Next.js 15 で短縮 staleTimes を設定

問題2: データ更新したのにページは古いまま

症状

DB を手動更新したり revalidatePath を呼んだのに、リロードしても古いデータのまま。

調査思考法:4層キャッシュを順にチェック

第1層:Router Cache(クライアント)

クライアントキャッシュが見落とされがちです。

クイックテスト:

  • Ctrl+Shift+R でハードリロード
  • これで更新されるなら Router Cache の問題

解決策:

'use client'

import { useRouter } from 'next/navigation'

export function RefreshButton() {
  const router = useRouter()

  return (
    <button onClick={() => router.refresh()}>
      更新
    </button>
  )
}

または next.config.js でキャッシュ時間を短縮:

module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 0,    // 動的ルートキャッシュ無効
      static: 30,    // 静的ルート30秒
    },
  },
}

第2層:Full Route Cache(サーバー)

ルートが静的か確認。静的なら HTML 全体がキャッシュされています。

クイックテスト:

# 新しいシークレットウィンドウでアクセス
# それでも古ければサーバーキャッシュ

解決策:

'use server'

import { revalidatePath } from 'next/cache'

export async function updateData() {
  await db.update({ ... })

  // ルートキャッシュをクリア
  revalidatePath('/your-page')
}

第 3 層:Data Cache(サーバー)

fetch リクエストの設定を確認します。

クイックテスト:

fetch にタイムスタンプログを追加:

const data = await fetch(url)
console.log('Fetched at:', new Date().toISOString())

ページをリロードし、タイムスタンプが変わらなければキャッシュを使用しています。

解決策:

方法 1:データに tag を付けて無効化

// データ取得時
const data = await fetch(url, {
  next: { tags: ['my-data'] }
})

// データ更新時
revalidateTag('my-data')

方法 2:テスト用に一時的にキャッシュを無効化

const data = await fetch(url, {
  cache: 'no-store'  // キャッシュしない
})

第 4 層:Request Memoization(サーバー)

この層は通常問題になりません。単一リクエスト内でのみ有効だからです。最初の 3 層に問題がなければ、キャッシュではなくデータソース自体の問題かもしれません。

問題 3:revalidatePath と revalidateTag、どちらを使う?

決定ツリー

キャッシュを無効化したい
    |
    ├─ 1 ページだけ影響
    |      → revalidatePath('/specific-page')
    |
    ├─ 1 パス配下のすべてのページに影響
    |      → revalidatePath('/blog', 'layout')
    |
    ├─ 複数の異なるパスのページで同じデータを使用
    |      → revalidateTag('your-tag')
    |
    └─ 即時無効化が必要(編集直後に反映)
           → updateTag('your-tag')(Next.js 15)

実践例:ブログシステム

ブログシステムに以下のページがあると仮定します:

  • トップページ:最新 3 記事を表示
  • ブログ一覧 /blog:すべての記事
  • 記事詳細 /blog/[slug]:単一記事
  • 著者ページ /author/[id]:著者のすべての記事

タグ付け戦略:

// データ取得時に tag を付与
async function getPosts() {
  return fetch('https://api.example.com/posts', {
    next: {
      revalidate: 3600,
      tags: ['posts']  // 記事関連データすべてにこの tag
    }
  })
}

async function getPostBySlug(slug) {
  return fetch(`https://api.example.com/posts/${slug}`, {
    next: {
      revalidate: 3600,
      tags: ['posts', `post-${slug}`]  // 汎用 tag と特定 tag
    }
  })
}

async function getPostsByAuthor(authorId) {
  return fetch(`https://api.example.com/posts?author=${authorId}`, {
    next: {
      revalidate: 3600,
      tags: ['posts', `author-${authorId}-posts`]
    }
  })
}

新記事公開時:

export async function publishPost(formData) {
  await db.posts.create({ ... })

  // 'posts' tag だけ無効化すれば OK
  // この tag を使うすべてのページが更新される
  revalidateTag('posts')
}

特定記事を修正時:

export async function updatePost(slug, newData) {
  await db.posts.update({ where: { slug }, data: newData })

  // この記事関連のキャッシュだけ無効化
  revalidateTag(`post-${slug}`)

  // 一覧ページも更新したい場合
  revalidateTag('posts')
}

問題 4:Next.js 14 から 15 にアップグレード後、キャッシュが動かなくなった

症状

Next.js 15 にアップグレード後、以前はうまくキャッシュされていたデータが毎回再リクエストされ、パフォーマンスが急落。

原因

Next.js 15 の 3 つのキャッシュデフォルト変更:

  1. fetch デフォルトが force-cache から no-store
  2. GET ルートハンドラーがデフォルト非キャッシュ
  3. Router Cache がデフォルト無効

移行方案

方案 1:明示的にキャッシュを有効化(推奨)

// 以前(Next.js 14)
const data = await fetch(url)

// 現在(Next.js 15)— 明示的宣言が必要
const data = await fetch(url, {
  cache: 'force-cache',
  next: { revalidate: 3600 }
})

方案 2:use cache ディレクティブを使用

'use cache'

export async function getPostList() {
  const posts = await db.posts.findMany()
  return posts
}

方案 3:Router Cache を有効化

// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
}

移行前後の比較:

// Next.js 14 — 暗黙的キャッシュ
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  // 自動キャッシュ
}

// Next.js 15 — 明示的キャッシュ
'use cache'  // この行を追加

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    cache: 'force-cache',  // またはこれを追加
    next: { revalidate: 3600 }
  })
}

私の提案:

ワンクリック移行は期待しないでください。すべてのデータリクエストを丁寧に見直し、意識的に決める — 何をキャッシュし、何をキャッシュしないか。面倒ですが、長期的には健全です。システム内で何がキャッシュされ、何がされていないか明確になります。

ベストプラクティスと選択戦略

ここまでで、どのキャッシュ戦略をいつ使うかをまとめます。

キャッシュ戦略選択フローチャート

データの更新頻度は?
    |
    ├─ ほぼ変わらない(About ページ、ヘルプドキュメント等)
    |      → 静的生成、revalidate 不要
    |      → 変更時に手動再デプロイ
    |
    ├─ 定期更新(毎時、毎日等)
    |      → ISR + 時間ベース revalidate
    |      → export const revalidate = 3600
    |
    ├─ 不定期更新(ユーザー投稿コンテンツ等)
    |      → オンデマンド revalidate
    |      → revalidatePath または revalidateTag
    |
    └─ リアルタイム更新(チャット、ライブデータ等)
           → 動的レンダリング + cache: 'no-store'
           → キャッシュしない

タグ命名戦略

revalidateTag を使う場合、以下の命名規則を推奨します。

粒度設計

  • 粗粒度(一括無効化向け)

    • posts — すべての記事
    • products — すべての商品
    • users — すべてのユーザー
  • 中粒度(カテゴリ/ステータス別)

    • posts:published — 公開済み記事
    • posts:draft — 下書き
    • products:category:electronics — 電子製品
  • 細粒度(特定リソース)

    • post:id:123 — ID 123 の記事
    • user:profile:456 — ID 456 のユーザープロフィール

命名規則の提案

名前空間を使い、entity:type:id 形式:

// データ取得時
const post = await fetch(`/api/posts/${id}`, {
  next: {
    tags: [
      'posts',                    // 粗粒度
      'posts:published',          // 中粒度
      `post:id:${id}`             // 細粒度
    ]
  }
})

// 無効化時に粒度を選択
revalidateTag('posts')              // すべての記事
revalidateTag('posts:published')    // 公開済みのみ
revalidateTag(`post:id:${id}`)      // 特定記事のみ

パフォーマンス最適化の提案

1. 過剰キャッシュを避ける

キャッシュは多いほど良いわけではありません。キャッシュしすぎると:

  • データ不整合
  • デバッグ困難
  • ストレージ浪費

経験則:

  • ユーザー個別データ(カート、個人設定)→ キャッシュしない
  • 公開データ(商品一覧、記事一覧)→ キャッシュする
  • リアルタイムデータ(在庫、オンライン人数)→ キャッシュしないか極短時間

2. revalidate 時間を適切に設定

短すぎる時間は避けましょう。revalidate: 1 なら毎秒再生成の可能性があり、キャッシュの意味がありません。

推奨設定:

  • ニュース:30〜60 分
  • ブログ記事:1〜2 時間
  • 商品カタログ:2〜4 時間
  • 静的ページ:24 時間以上

3. stale-while-revalidate を活用

Next.js 15 の profile="max" がこの戦略です:

revalidateTag('posts', { profile: 'max' })

ユーザーは常にキャッシュ内容を見る(高速!)、システムはバックグラウンドで静かに更新。最高の UX です。

4. キャッシュヒット率を監視

.env.local に追加:

NEXT_PRIVATE_DEBUG_CACHE=1

本番サーバーを起動すると、コンソールにキャッシュヒット状況が表示されます:

○ GET /blog 200 in 45ms (cache: HIT)
○ GET /about 200 in 12ms (cache: SKIP)

定期的に確認し、キャッシュ戦略が有効かチェックしましょう。

開発環境と本番環境の違い

重要な注意:

開発環境(npm run dev)と本番環境(npm start)ではキャッシュ挙動がまったく異なります。

特性開発環境本番環境
Data Cache大部分無効完全有効
Full Route Cache無効静的ルート有効
Request Memoization有効有効
Router Cache有効だが極短フル時間

キャッシュ機能をテストする正しい手順:

# 1. 本番ビルド
npm run build

# 2. ターミナル出力でルートタイプを確認
#    ○ = 静的、キャッシュされる
#    ƒ = 動的、キャッシュされない

# 3. 本番サーバー起動
npm start

# 4. キャッシュ挙動をテスト
# ブラウザでページにアクセスし、データソースを変更
# リロードして古いデータのままか確認

# 5. revalidate をテスト
# revalidate 時間経過後に再アクセスし、更新されるか確認

開発環境でキャッシュ問題をデバッグしないでください。最もよくある落とし穴です。開発環境で半日悩んで revalidate が動かないと不満を言う人をたくさん見てきました。

結論

ここまでで、核心は以下の数点です。

1. 4 層キャッシュの役割を理解する

混同しないこと。Router Cache はクライアント側、残り 3 つはサーバー側。revalidate は主に Data Cache と Full Route Cache に影響します。

2. 適切な revalidate 方法を選ぶ

  • 定期更新export const revalidate = 3600
  • ユーザー操作、単一ページrevalidatePath('/page')
  • ユーザー操作、複数ページrevalidateTag('tag')
  • 即時無効化updateTag('tag')(Next.js 15)

3. 本番環境でテスト

覚えておいて:開発環境のキャッシュ挙動は不正確です。キャッシュをテストするなら npm run build && npm start が必須。

4. 層ごとにトラブルシューティング

データが更新されないとき、この順序で確認:

  1. Router Cache(クライアント)→ ハードリロード
  2. Full Route Cache(サーバー)→ revalidatePath
  3. Data Cache(サーバー)→ revalidateTag
  4. データソース自体

5. 明示は暗黙に勝る(Next.js 15 の哲学)

Next.js 15 はキャッシュをデフォルト有効からデフォルト無効に変更しました。どのデータをキャッシュするか能動的に考える必要があります。面倒ですが、長期的にはコードが明確で保守しやすくなります。


ここまで読んだなら、Next.js のキャッシュメカニズムを完全に理解したことになります。次にデータ不更新問題に直面したとき、どこから手を付けるか分かるはずです。

キャッシュは複雑ですが、理解すれば Next.js 最強の機能の一つだと分かります。適切に使えばアプリは爆速に;適当に使えば自分で穴を掘ることになります。

アプリのパフォーマンスが爆上がりし、バグがゼロであることを祈っています!

Next.js キャッシュメカニズム完全利用フロー

4 層キャッシュの理解から revalidate 方法の選択、データ不更新問題のトラブルシューティングまでの完全手順

⏱️ 目安時間: 2 時間

  1. 1

    ステップ1: Next.js 4 層キャッシュを理解する

    4 層キャッシュ:

    1. Request Memoization(リクエスト重複排除)
    • 同一リクエスト内で同じ URL への fetch は 1 回だけ
    • 自動処理、設定不要
    • ライフサイクル:単一リクエスト

    2. Data Cache(fetch キャッシュ)
    • fetch リクエストのキャッシュ
    • デフォルト:Next.js 14 はキャッシュ、15 は非キャッシュ
    • 設定:cache オプション

    3. Full Route Cache(完全ルートキャッシュ)
    • ルート全体の HTML キャッシュ
    • 静的ページは自動キャッシュ
    • 設定:revalidate

    4. Router Cache(クライアントルートキャッシュ)
    • クライアントナビゲーション時のキャッシュ
    • 自動処理、設定不要
    • ライフサイクル:セッション中

    ポイント:Router Cache はクライアント側、残り 3 つはサーバー側。
  2. 2

    ステップ2: 適切な revalidate 方法を選ぶ

    3 つの方法:

    1. 定期更新(export const revalidate)
    ```tsx
    export const revalidate = 3600 // 3600 秒後に再検証
    ```
    • 適用:定期更新コンテンツ
    • 設定:page.tsx または layout.tsx

    2. ユーザー操作、単一ページ(revalidatePath)
    ```tsx
    import { revalidatePath } from 'next/cache'

    revalidatePath('/blog/post-1')
    ```
    • 適用:ユーザー操作後に単一ページを更新
    • 使用:Server Action または API Route

    3. ユーザー操作、複数ページ(revalidateTag)
    ```tsx
    import { revalidateTag } from 'next/cache'

    // fetch 時に tag を付与
    fetch(url, { next: { tags: ['posts'] } })

    // 更新時に tag をクリア
    revalidateTag('posts')
    ```
    • 適用:ユーザー操作後に複数ページを更新
    • 使用:Server Action または API Route

    選択の目安:
    • 定期更新 → export const revalidate
    • 単一ページ → revalidatePath
    • 複数ページ → revalidateTag
  3. 3

    ステップ3: データ不更新問題をトラブルシューティング

    調査順序:

    1. 開発環境か確認
    • 開発環境ではキャッシュ挙動が不正確
    • 本番環境でテスト必須:npm run build && npm start

    2. Router Cache(クライアント)を確認
    • ハードリロード(Ctrl+Shift+R)を試す
    • ブラウザキャッシュをクリア

    3. Full Route Cache(サーバー)を確認
    • revalidatePath でクリア
    • revalidate 設定を確認

    4. Data Cache(サーバー)を確認
    • revalidateTag でクリア
    • fetch の cache 設定を確認

    5. データソースを確認
    • データソースが本当に更新されたか
    • API 返却データを確認

    よくある原因:
    • 開発環境でテスト → 本番環境を使う
    • Router Cache 未クリア → ハードリロード
    • revalidate 設定ミス → 設定を確認
    • Next.js 15 デフォルト非キャッシュ → cache を明示設定
  4. 4

    ステップ4: Next.js 14 と 15 のキャッシュ差異

    Next.js 14:
    • fetch デフォルトキャッシュ(getStaticProps 相当)
    • 明示的に無効化:cache: 'no-store'

    Next.js 15:
    • fetch デフォルト非キャッシュ
    • 明示的に有効化:cache: 'force-cache'

    移行の提案:
    • すべての fetch 呼び出しを確認
    • cache オプションを明示設定
    • キャッシュ挙動をテスト

    コード例:
    ```tsx
    // Next.js 14(デフォルトキャッシュ)
    fetch(url) // 自動キャッシュ

    // Next.js 15(デフォルト非キャッシュ)
    fetch(url, { cache: 'force-cache' }) // 明示設定が必要
    ```

    ポイント:Next.js 15 の哲学は「明示は暗黙に勝る」。どのデータをキャッシュするか能動的に考える必要がある。

FAQ

Next.js のキャッシュは何層?各層は何を管轄する?
4 層キャッシュ:

1. Request Memoization(リクエスト重複排除)
• 同一リクエスト内で同じ URL への fetch は 1 回だけ
• 自動処理、設定不要
• ライフサイクル:単一リクエスト

2. Data Cache(fetch キャッシュ)
• fetch リクエストのキャッシュ
• デフォルト:Next.js 14 はキャッシュ、15 は非キャッシュ
• 設定:cache オプション

3. Full Route Cache(完全ルートキャッシュ)
• ルート全体の HTML キャッシュ
• 静的ページは自動キャッシュ
• 設定:revalidate

4. Router Cache(クライアントルートキャッシュ)
• クライアントナビゲーション時のキャッシュ
• 自動処理、設定不要
• ライフサイクル:セッション中

ポイント:Router Cache はクライアント側、残り 3 つはサーバー側。revalidate は主に Data Cache と Full Route Cache に影響する。
revalidatePath、revalidateTag、updateTag の違いは?
revalidatePath(単一ページ):
• 指定パスのキャッシュをクリア
• 適用:ユーザー操作後に単一ページを更新
• 使用:revalidatePath('/blog/post-1')

revalidateTag(複数ページ):
• 指定 tag のすべてのキャッシュをクリア
• 適用:ユーザー操作後に複数ページを更新
• 使用:fetch 時に tag を付与し、更新時に tag をクリア

updateTag(即時無効化、Next.js 15):
• tag を即座に期限切れとしてマーク
• 適用:即時無効化が必要なシーン
• 使用:updateTag('tag')

選択の目安:
• 単一ページ → revalidatePath
• 複数ページ → revalidateTag
• 即時無効化 → updateTag(Next.js 15)

注意:revalidateTag と updateTag は fetch の tags と組み合わせて使う。
revalidate を設定してもデータが更新されないのはなぜ?
考えられる原因:

1. 開発環境でテストしている
• 開発環境ではキャッシュ挙動が不正確
• 本番環境必須:npm run build && npm start

2. Router Cache がクリアされていない
• クライアントキャッシュが残っている
• ハードリロード(Ctrl+Shift+R)を試す

3. revalidate 設定ミス
• revalidate 値を確認
• 設定位置を確認

4. Next.js 15 デフォルト非キャッシュ
• cache: 'force-cache' を明示設定
• fetch の cache 設定を確認

5. データソース自体が更新されていない
• データソースが本当に更新されたか確認
• API 返却データを確認

調査順序:
1. 開発環境か確認
2. Router Cache(ハードリロード)
3. Full Route Cache(revalidatePath)
4. Data Cache(revalidateTag)
5. データソース
Next.js 14 と 15 のキャッシュの違いは?
主な違い:

Next.js 14:
• fetch デフォルトキャッシュ(getStaticProps 相当)
• 明示的に無効化:cache: 'no-store'
• 挙動:暗黙的キャッシュ

Next.js 15:
• fetch デフォルト非キャッシュ
• 明示的に有効化:cache: 'force-cache'
• 挙動:明示的設定

移行の提案:
• すべての fetch 呼び出しを確認
• cache オプションを明示設定
• キャッシュ挙動をテスト

コード例:
```tsx
// Next.js 14(デフォルトキャッシュ)
fetch(url) // 自動キャッシュ

// Next.js 15(デフォルト非キャッシュ)
fetch(url, { cache: 'force-cache' }) // 明示設定が必要
```

ポイント:Next.js 15 の哲学は「明示は暗黙に勝る」。どのデータをキャッシュするか能動的に考える必要がある。破壊的変更のため、移行時は注意が必要。
revalidatePath と revalidateTag はいつ使い分ける?
revalidatePath(単一ページ):
• 適用:ユーザー操作後に単一ページを更新
• 例:記事編集後に記事詳細ページを更新
• 使用:revalidatePath('/blog/post-1')

revalidateTag(複数ページ):
• 適用:ユーザー操作後に複数ページを更新
• 例:新記事公開後にすべての一覧ページを更新
• 使用:fetch 時に tag を付与し、更新時に tag をクリア

選択の目安:
• 1 ページだけ更新 → revalidatePath
• 複数ページを更新 → revalidateTag

コード例:
```tsx
// 単一ページ
revalidatePath('/blog/post-1')

// 複数ページ
fetch(url, { next: { tags: ['posts'] } })
revalidateTag('posts') // 'posts' tag 付きキャッシュをすべてクリア
```

ポイント:revalidateTag は fetch の tags と組み合わせて使い、より柔軟。
開発環境と本番環境のキャッシュ挙動は同じ?
同じではありません。開発環境ではキャッシュ挙動が不正確です。

開発環境:
• キャッシュ挙動が不安定
• キャッシュされない場合がある
• キャッシュテストに不向き

本番環境:
• キャッシュ挙動が正確
• 正常にキャッシュされる
• キャッシュテストに適している

キャッシュのテスト:
• 本番環境必須:npm run build && npm start
• 開発環境でキャッシュをテストしない

よくあるミス:
• 開発環境でキャッシュをテスト → 結果が不正確
• revalidate が動かないと不満 → 実は環境の問題

提案:キャッシュ挙動は常に本番環境でテストする。

9分で読めます · 公開日: 2025年12月19日 · 更新日: 2026年6月8日

関連記事

コメント

GitHubアカウントでログインしてコメントできます