Next.jsのキャッシュは、バージョン15と16で既定の挙動が変わり、2026年10月6日公開の16.4で推奨の書き方も決まりました。16.4では、use cacheで必要な箇所だけをキャッシュするCache Componentsが「すべてのNext.jsアプリに推奨する方式」とされ、create-next-appで作る新規アプリでは最初から有効になっています。「App Routerは既定でキャッシュする」という13〜14当時の解説は、現在の仕様と違います。この記事では16.4.0を基準に、保存先・再検証API・効かないときの切り分けを、next buildとnext startで確かめた結果を交えて整理します。
まとめ:Next.jsのキャッシュで押さえる4点
- 方式:新規アプリはCache Components(
cacheComponents: trueとpartialPrefetching: true)で書く。データ取得の結果はuse cacheを付けた関数・コンポーネントだけがキャッシュされ、未キャッシュの取得やCookieなどリクエスト依存の部分はリクエスト時に実行される。 - 寿命:
use cacheには寿命を明確にするためcacheLifeを明示する(省略も仕様上は可能)。省略するとdefault(サーバー側の再検証間隔15分)が適用される。15分たつと、その後のアクセスを契機に作り直される。 - 無効化:操作した本人にすぐ見せるならServer Action内の
updateTag、多少遅れてよいならrevalidateTag(tag, 'max')。 - デプロイ:
use cacheのエントリはキャッシュキーにビルドID(設定時はdeploymentId)を含むため、そのIDが変わると以前のエントリは使われなくなる。デプロイ後に古い画面が出るなら、まずCDNとブラウザのHTTPキャッシュを疑う。
なお本稿が扱うのは実行時のキャッシュです。ビルドを速くするTurbopackのディスクキャッシュは別物で、Next.js 16.3とは?Turbopack永続キャッシュとInstant Navigationsなど新機能を実装者目線で解説で扱っています。
キャッシュの保存先と旧4分類の対応
Next.jsの「キャッシュ」は、保存場所も寿命も異なる複数の層の総称です。キャッシュされた関数の出力はRSCペイロードにシリアライズされ、HTMLとして置かれるか、サーバーに残るか、ブラウザへ送られるかが違うだけです(公式のCachingガイド)。ペイロードの中身はRSC Payload(RSCペイロード)とは?中身の読み方と取得手順で解説しています。
| 層 | 保存先 | 寿命の決め方 | 13〜14の呼び名 |
|---|---|---|---|
| プリレンダーHTML・ISR | ディスク+メモリ50MB | revalidate / expire | Full Route Cache |
| fetch・unstable_cacheの結果 | ディスク+メモリ50MB | revalidate | Data Cache |
| use cacheの結果 | 各インスタンスのメモリ | cacheLife | (16で新設) |
| 共有キャッシュ | Redis等の外部ストア | cacheLife・revalidate | (なし) |
| クライアントキャッシュ | ブラウザのメモリ | stale(最低30秒) | Router Cache |
| HTTPキャッシュ | CDN・ブラウザ | Cache-Control | (Next.jsの外) |
旧4分類のうちRequest Memoizationは、1回のレンダリング内で同じfetchをまとめるだけでレンダリングをまたいで残らないため、表には入れていません。
サーバー側:インスタンスごとに分かれる2つの保存先
サーバー側には仕組みの違う保存先が2つあります。ページの生成結果・ISR・fetchとunstable_cacheの結果を持つ従来のサーバーキャッシュは、各インスタンスのディスクとメモリ(cacheMaxMemorySizeの既定値50MB)に置かれます(Self-Hostingガイド)。一方、use cacheの既定ハンドラはメモリ上のLRUだけを使い、サーバーレスではインスタンスが使い捨てられるのでリクエストをまたいでほぼ残りません。Kubernetesのように複数インスタンスを並べると、どちらもPodごとに別々のキャッシュになります。
共有したい場合の設定も別々です。use cacheはuse cache: remoteかcacheHandlers(複数形)で外部ストアのハンドラを指定します。従来のサーバーキャッシュはcacheHandler(単数形)を指定し、cacheMaxMemorySize: 0でインスタンスごとのメモリキャッシュを止めます。
もう1つの落とし穴が無効化の伝わり方です。revalidateTagは既定では呼び出したインスタンスのキャッシュしか消しません。他のインスタンスへ伝えるには、共有ストアに無効化の記録を置きます。use cache用のcacheHandlersではupdateTags()(無効化を共有ストアへ書く)とrefreshTags()(リクエスト前に読みに行く)の両方を、従来方式のcacheHandlerではrevalidateTag()メソッドを実装します(How Revalidation Works)。
ブラウザ側:最低30秒のクライアントキャッシュ
クライアントルーターは、プリフェッチやナビゲーションで受け取ったRSCペイロードをブラウザのメモリに保持します。寿命はcacheLifeのstale値で、サーバーはx-nextjs-stale-timeレスポンスヘッダーで伝えます。cacheLife('hours')のページを実測するとx-nextjs-stale-time: 300が返りました。ここには最低30秒の下限があり、それより短くはできません。Server Actionから再検証系のAPIを呼ぶと、stale時間に関係なくクライアントキャッシュは即座にクリアされます。
バージョンごとに変わった既定値
解説記事を読むときは、どの版を前提にしているかを先に確かめてください。既定値が版ごとに変わっているためです。公式リリース記事の日付は15が2024年10月21日、16が2025年10月21日、16.3が2026年8月3日、16.4が2026年10月6日です。
| 対象 | 13〜14 | 15 | 16.0〜16.3 | 16.4 |
|---|---|---|---|---|
| fetch | 既定でキャッシュ | 非キャッシュ | 非キャッシュ | 非キャッシュ |
| GET Route Handler | 既定でキャッシュ | 非キャッシュ | 非キャッシュ | 非キャッシュ |
| staleTimes.dynamic | 30秒 | 0秒 | 0秒 | 0秒 |
| cacheComponents | なし | 実験的 | 任意で有効化 | 推奨・新規は既定で有効 |
| partialPrefetching | なし | なし | 16.3で追加 | 推奨構成で有効 |
15での変更は、fetch・GET Route Handler・クライアントのルーターキャッシュを「キャッシュする」から「しない」へ倒すものでした(Next.js 15のリリース記事)。16ではexperimental.dynamicIOとexperimental.pprがcacheComponentsにまとめられました。Next.js本体のcacheComponentsの既定値は16.4でもfalseなので、既存アプリを16.4へ上げただけでは有効になりません。16.4のリリース記事は、次のメジャーである17でCache Componentsを既定にすると予告しています。版ごとの変更点とアップデート手順はNext.js 16の最新バージョンは16.3|リリース履歴・LTS方針・アップデート手順にまとめています。
Cache Componentsを有効にする設定
既存アプリで有効にするにはnext.config.tsに2つのフラグを立てます。[email protected]はこの2行を既定で書き込みます。
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
export default nextConfig
partialPrefetchingを書かずにnext buildすると、「cacheComponents is enabled without a corresponding partialPrefetching option」という警告が出ます。partialPrefetchingは16.3.0で追加された設定で、falseにするのはPartial Prefetchingより前にCache Componentsを導入済みのアプリを移行している途中だけです。有効化するとPartial Prerendering(PPR)が既定になり、静的なシェルを先に返してリクエスト依存の部分だけを後から流します。Node.jsランタイムが必須なので、export const runtime = 'edge'を残しているルートは移行が必要です。
use cacheによるデータとUIのキャッシュ
use cacheはファイル先頭、コンポーネント本体の先頭、関数本体の先頭のいずれにも置けます。対象はasync関数に限られます。キャッシュキーはビルドID、関数の位置と署名、引数から作られ、外側のスコープから参照した変数も自動的に引数としてキーに入ります。
静的UI・キャッシュ・ストリーミングを組み合わせたページ実装
次の例はapp/blog/page.tsxに置く想定で、@/lib/dbはアプリ側で用意するDBアクセス層です。見出しは静的、記事一覧は1時間を過ぎた後のアクセスを契機に背後で作り直し、Cookieを読むテーマ表示はリクエスト時にストリーミングします。
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
import { db } from '@/lib/db'
export default function BlogPage() {
return (
<>
<h1>ブログ</h1>
<PostList />
<Suspense fallback={<p>読み込み中…</p>}>
<Theme />
</Suspense>
</>
)
}
async function PostList() {
'use cache'
cacheLife('hours')
cacheTag('posts')
const posts = await db.posts.list()
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
}
async function Theme() {
const theme = (await cookies()).get('theme')?.value ?? 'light'
return <p>テーマ: {theme}</p>
}
[email protected]でビルドすると、このルートは◐ /blog 1h 1d(Partial Prerender、Revalidate 1時間、Expire 1日)と表示されました。Cookieを読んでもルート全体が動的レンダリングに倒れることはなく、見出しと記事一覧は静的シェルに入ります。cookies()やheaders()をuse cacheの内側で読むことはできないので、外で値を取り出して引数で渡します。
cacheLifeのプリセット値と短命キャッシュの扱い
cacheLifeはstale(クライアントが確認なしで使える時間)、revalidate(サーバーが背後で作り直すまでの時間)、expire(アクセスが無いまま経過したら同期再生成に切り替わる時間)の3つを持ちます。[email protected]の実装(dist/server/config-shared.js)の値は次のとおりで、cacheLifeのリファレンスの表とも一致します。
| プロファイル | stale | revalidate | expire |
|---|---|---|---|
| default | 5分 | 15分 | 期限なし |
| seconds | 30秒 | 1秒 | 1分 |
| minutes | 5分 | 1分 | 1時間 |
| hours | 5分 | 1時間 | 1日 |
| days | 5分 | 1日 | 1週間 |
| weeks | 5分 | 1週間 | 30日 |
| max | 5分 | 30日 | 1年 |
寿命が短すぎるキャッシュはプリレンダーから外れます。revalidateが0かexpireが5分未満なら、リクエスト時に解決される「動的な穴」になり、プリセットではsecondsだけが該当します。staleが30秒以上5分未満なら、プリレンダーには入りますが、プリフェッチで配るApp Shellからは外れます。入れ子にも注意が要ります。cacheLifeを書いていない外側のuse cacheが短命キャッシュを含むと、外側まで短命化するのを防ぐためにビルドがエラーで止まります。外側にcacheLifeを明示すれば解決します。
use cache・private・remoteの選び分け
| ディレクティブ | サーバー保存 | cookies等 | 向く用途 |
|---|---|---|---|
| use cache | インスタンスのメモリ | 不可 | 全員共通のデータ |
| use cache: remote | 共有ハンドラ | 不可 | ヒット率が高い重い処理 |
| use cache: private | しない | 可 | ユーザー固有の表示 |
remoteは毎回ネットワーク往復が入るので、ヒット率が高いときしか元が取れません。privateの結果はブラウザのメモリにだけ残り、再読み込みで消えます。まずはCookieの値を外で読んでuse cacheの引数に渡す形を試し、書けないときだけprivateを使います。
ensureStaticによる静的レンダリングの保証
16.4で追加されたexport const ensureStatic = 'navigation'をページやレイアウトに書くと、そのルートに動的な部分が紛れ込んだ時点でビルドが失敗します。上の/blogに付けて試すと、Cookieを読むThemeのせいでNext.js encountered uncached or runtime data on a route that must be fully static.というエラーで止まりました。値は'shell'・'prefetch'・'navigation'の順に厳しくなります。
再検証APIの使い分け
16では再検証APIの役割が整理されました。fetchのnext.tagsで付けたタグにもcacheTagで付けたタグにも同じように効き、cacheComponentsの有無を問わず使えます。
| API | 呼べる場所 | 次の読み取り | 使う場面 |
|---|---|---|---|
| revalidateTag(tag, ‘max’) | Server Action・Route Handler | 旧データを返し背後で更新 | Webhook・一覧 |
| revalidateTag(tag, { expire: 0 }) | Server Action・Route Handler | 新データを待つ | 外部からの即時削除 |
| updateTag(tag) | Server Actionのみ | 新データを待つ | フォーム送信・設定変更 |
| revalidatePath(path) | Server Action・Route Handler | パス単位で無効化 | タグ設計の無いルート |
| refresh() | Server Actionのみ | 未キャッシュ部分だけ更新 | 通知件数 |
'use server'
import { revalidateTag, updateTag, refresh } from 'next/cache'
import { db } from '@/lib/db'
type Profile = { name: string }
export async function publishPost(id: string) {
await db.posts.publish(id)
revalidateTag('posts', 'max')
}
export async function saveProfile(id: string, data: Profile) {
// 認証・更新権限・入力値の検証はここか db 層で行う前提
await db.users.update(id, data)
updateTag(`user-${id}`)
}
export async function markAsRead(id: string) {
await db.notifications.markAsRead(id)
refresh()
}
判断の基準は「更新した本人にすぐ見せる必要があるか」です。フォーム送信のように本人が結果を確認する導線では、読み取り直しを保証するupdateTagを選びます。記事一覧のように多少の遅れが許されるものはrevalidateTagに'max'を渡し、閲覧者には旧データを即座に返しながら背後で作り直させます。仕組みはstale-while-revalidateとは?Cache-ControlとSWRの再検証を解説を参照してください。
第2引数は省略できません。revalidateTag('posts')と書いてビルドすると、16.4ではerror TS2554: Expected 2 arguments, but got 1.で型チェックが通りませんでした。型エラーを抑えれば動きますが、挙動は{ expire: 0 }と同じになります(revalidateTagのリファレンス)。
CDNのキャッシュ可否とHTML・RSCのパージ対象
サーバーキャッシュを正しく書いても、手前のCDNが別の判断をすれば古い画面は出続けます。[email protected]をnext startで起動して確かめたヘッダーが次の表です。
| ルート | Cache-Control |
|---|---|
| use cacheだけの静的ページ(hours) | s-maxage=3600, stale-while-revalidate=82800 |
| Suspenseで動的部分を含むページ | private, no-cache, no-store, max-age=0, must-revalidate |
| /_next/static配下のJS | public, max-age=31536000, immutable |
静的ページのstale-while-revalidateは、expire(1日)からrevalidate(1時間)を引いた値です。一方、動的な部分を1つでも含むPPRのページは、静的シェルがプリレンダー済みでもHTMLのレスポンスにprivate, no-storeが付き、一般的なCDNはこれをキャッシュしません。公式は静的シェルをCDNから直接配信できると説明していますが、それはPPRに対応したデプロイ先やアダプターでシェルを分けて配る場合の話です。next startの前にCDNを置くだけの構成でページをCDNに載せたいなら、そのルートを完全に静的にします。
逆に、CDNがキャッシュしている静的ページではrevalidateTagやrevalidatePathがCDNまで届きません。公式のCDN Cachingガイドも、CDNはs-maxageが切れるまで手元の複製を返し続けるので、再検証と同時にCDNのパージAPIを呼ぶよう案内しています。パージ対象にはHTMLとRSCの両方が要ります。RSCのリクエストはクエリ_rscで区別され、実測ではrsc: 1ヘッダーだけのリクエストに?_rsc付きURLへの307が返りました。クエリをキャッシュキーから外すCDNでは画面遷移が壊れます。CDN側の設定の考え方はCDNとは?仕組み・キャッシュ制御からサービスの選び方まで実装目線で解説を参照してください。
Cache-Controlの全パス一括指定による長期キャッシュの危険
headers()で全パスに長いキャッシュ期間を付ける例を見かけますが、採用すべきではありません。
// 採用しないこと
module.exports = {
async headers() {
return [{
source: '/(.*)',
headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
}]
},
}
この値は本来、ファイル名にハッシュを含む不変アセットにNext.jsが自動で付けるもので、公式はそのアセットについては上書きできないと書いています(headersのリファレンス)。問題は残りのパスです。15以降のNext.jsはカスタムのCache-Controlを既定値で上書きしなくなったため、HTMLページにも書いた値がそのまま出ます。immutable付きなので再検証も行われず、一度アクセスした人には何度デプロイしても最大1年間古いページが表示されます。手を入れるなら/images/:path*のように自分で管理する静的パスに限ります。
cacheComponentsを使わない構成での書き方
既存アプリを16へ上げただけの場合、キャッシュは従来どおり個別に指定します。この構成でもfetchは既定でキャッシュされません(Caching (Previous Model))。
import { unstable_cache } from 'next/cache'
import { db } from '@/lib/db'
// fetch を明示的にキャッシュする
export async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache',
next: { revalidate: 3600, tags: ['posts'] },
})
return res.json()
}
// fetch を使わない DB クエリをキャッシュする
export const getCachedUser = unstable_cache(
async (id: string) => db.users.findUnique({ where: { id } }),
['user'],
{ tags: ['user'], revalidate: 3600 }
)
ルート単位なら、ページやレイアウトからexport const revalidate = 600のように出します。ルート内で最も小さいrevalidateがルート全体の間隔になります。レンダリング方式の選び方はNext.jsのレンダリング方式|SSG・ISR・SSR・CSRの違いとApp Routerでの選び方で整理しています。
unstable_cacheは公式に「16でuse cacheに置き換えられた」と明記されています。それでもこの構成を残す理由は1つで、unstable_cacheとfetchのキャッシュは、保存先を維持する構成ならデプロイをまたいで再利用できるからです。セルフホストでコンテナを入れ替えるなら、永続ボリュームか外部ストアで保存先を残す必要があります。デプロイのたびに温め直すと外部APIの料金や負荷が問題になるデータだけ、この方式に残す判断はありえます。旧方式と新方式が混在する既存アプリの移行計画から相談したい場合は、業務用・Webアプリ開発の支援も行っています。
キャッシュが効かない・消えないときの切り分け
症状から原因の層を絞り込む手順です。上から順に確かめます。
- 開発サーバーで本番とキャッシュの挙動が違う →
next devではページは常にオンデマンドで描画されますが、データ側のキャッシュは開発時にも働きます。ページのキャッシュはnext buildとnext startで確かめます。 - デプロイ後も古い内容が出る →
use cacheはキーに使うID(ビルドIDかdeploymentId)が変われば旧エントリを使いません。IDが変わったか、旧版のインスタンスに振り分けられていないか、デプロイをまたぐfetch・unstable_cacheのキャッシュ、CDNとブラウザのHTTPキャッシュを順に確かめます。 - revalidateTagを呼んだのに古い → まず第2引数を見ます。
'max'なら、次のアクセスで作り直しが始まり、終わるまでのリクエストに旧データが返るのは正常な動作です。作り直しが終わっても古いなら、タグ名の一致、CDNが持っている複製、別インスタンスへの振り分けを確かめます。CDNはパージ、別インスタンスはキャッシュハンドラに共有ストアへの無効化の伝達を実装して直します。 - 本番でだけ
use cacheがエラーになる → キャッシュの内側(呼び出し先の関数を含む)でcookiesやheadersを読んでいます。動的なルートではnext buildを通過しnext startで落ちます。 - ビルドが50秒で止まる →
paramsやsearchParams、未キャッシュのデータをuse cacheの内側で待っています。値を先に取り出して引数で渡します。 - 戻る操作で古い画面が出る → クライアントキャッシュです。クライアントコンポーネントから
router.refresh()を呼びます。ただしサーバー側のキャッシュは無効化されないので、そこが古ければタグやパスの再検証も要ります。next/cacheのrefresh()はServer Action専用の別APIです。
挙動を追うときはNEXT_PRIVATE_DEBUG_CACHE=1を付けて起動すると詳細ログが出ます。開発時はキャッシュされた関数のコンソール出力にCacheの接頭辞が付きます。ただしこれはキャッシュ済みの関数のログを再生した印で、接頭辞だけではヒットか新規実行かを判別できません。
よくある質問
Next.jsのキャッシュをクリアするにはどうすればよいですか?
層ごとに手段が違います。use cacheの旧エントリは、キーに使うビルドID(設定時はdeploymentId)が変われば使われなくなります。同じIDのまま再配備しただけでは消える保証はありません。特定のデータだけなら、Server Action内はupdateTag、Route HandlerからはrevalidateTag、ルート単位はrevalidatePathです。CDNが持っている分はCDNのパージが別に要ります。
Data Cacheはどこに保存されますか?
13〜14でData Cacheと呼ばれたfetchとunstable_cacheの保存先は、セルフホストでは各インスタンスのディスクとメモリ(上限50MB)です。use cacheのキャッシュはこれとは別の仕組みで、既定では各インスタンスのメモリだけに置かれます。複数のコンテナで共有するなら、従来方式はcacheHandler、use cacheはuse cache: remoteかcacheHandlersでRedisなどの外部ストアに載せます。
Lighthouseの「効率的なキャッシュ保存期間を使用する」はどう直しますか?
HTTPキャッシュの指摘です。ビルド出力のJavaScriptとCSSにはNext.jsが1年のimmutableを自動で付けるので、指摘されるのはpublic/配下に自分で置いた画像やフォント、外部ドメインの配信物です。自サイトが配信するパスならheaders()か管理下のCDNでキャッシュ期間を付けます。外部ドメインの配信物は配信元で直すしかありません。同じURLのまま画像を差し替える運用なら、差し替えの頻度に合わせて期間を短めに決めます。
SSRのページもキャッシュできますか?
できます。Cache Componentsでは、リクエスト時のデータを読む部分だけを<Suspense>で囲み、残りをuse cacheで事前レンダリングします。ただしHTMLのレスポンスにはprivate, no-storeが付くため、CDNでページ丸ごとをキャッシュする用途には向きません。
ヘッドレスCMSの更新をすぐサイトへ反映するには?
キャッシュ関数にcacheTagでタグを付け、CMSのWebhookから呼ぶRoute HandlerでrevalidateTag('posts', 'max')を実行します。Route HandlerではupdateTagが使えないので、次の閲覧から必ず新しい内容を出したいときはrevalidateTag('posts', { expire: 0 })にします。下書きのプレビューにはDraft Modeを使います。その間はキャッシュが迂回されますが、有効化用のRoute Handlerと秘密トークンの検証は別に実装が要ります(Draft Modeガイド)。