Claude CodeでNext.jsのキャッシュを見直す — 古いデータの切り分け
Next.jsで更新したはずのデータが古いまま出るとき、next buildの出力とキャッシュの層を手がかりに原因を絞り、Claude Codeに直させる手順をまとめます。
Next.jsで更新したはずのデータが画面に出ないとき、原因は「どの層のキャッシュが効いているか」でほぼ決まります。Claude Codeには、症状だけでなくnext buildの出力と該当コードをセットで渡すと、直す箇所が絞れます。ここでは、キャッシュが効きすぎる場合と効かなさすぎる場合を、ビルド出力の読み方から順にたどります。
最初に確かめるのはcacheComponentsが有効かどうか
Next.js 16には、キャッシュの考え方が2つあります。next.config.tsにcacheComponents: trueがあれば、use cacheディレクティブで明示したものだけをキャッシュするモデルです。無ければ、fetchのオプションやルートセグメント設定で制御する従来のモデルになります。
cacheComponentsを有効にすると、App Routerのデータ取得はキャッシュを明示しない限りプリレンダリングの対象から外れます。従来モデルでは、fetchは既定でキャッシュされず、cache: 'force-cache'などで個別に有効にします。
2つのモデルは使うAPIが違います。検索や過去のコードにはどちらの書き方も混ざっているため、Claude Codeに依頼する前に次の1行を確かめておくと、別モデルのAPIを持ち込まれにくくなります。
grep -n "cacheComponents" next.config.*ヒットしなければ従来モデルです。以降の節は、この結果で読む箇所が変わります。
next buildの出力で静的と動的を読む
古いデータの切り分けは、ルートが静的か動的かを知るところから始まります。next buildを実行すると、ルートごとに記号が付きます。
| 記号 | 意味 |
|---|---|
○(Static) | 意味静的コンテンツとしてプリレンダリングされた |
ƒ(Dynamic) | 意味リクエストごとにサーバーで描画される |
◐(Partial Prerender) | 意味静的HTMLとして用意され、動的な部分がストリームで届く |
◐はCache Componentsのモデルで現れる記号で、RevalidateとExpireの列も付きます。古いデータが出るルートが○なら、ビルド時点の内容が残っている可能性があります。毎回遅いルートがƒなら、キャッシュが効いていません。
Claude Codeには出力をそのまま貼って、ルートごとに「期待した記号か」を照らし合わせさせます。
next buildの出力を貼ります。/products は更新が反映されない、
/dashboard は毎回遅い、という症状です。
各ルートの記号(○ ƒ ◐)と、そうなっている理由を
コードから特定してください。まだ修正はしないでください。再現コマンドはnext buildとnext startです。これをそのままClaude Codeに渡せるようにしておきます。
従来モデルで古いデータが出る型
cacheComponentsを使っていない場合、見るべき設定はfetchのオプション、unstable_cache、ルートセグメント設定の3か所です。
効きすぎて古くなる
fetchが既定のままでも、ビルド時に1回だけ取得される: オプション無しのfetchは、開発中は毎回取得されます。一方next buildでは、ルートが静的にプリレンダリングされるため1回だけ取得します。開発では新しく見えるのに本番で古い、という食い違いはここで起きますrevalidateが長い、またはfalse: ルートセグメントのrevalidateの既定はfalseで、実質的に無期限のキャッシュです。next.revalidateを指定しても、ルート全体で最小の値が頻度を決めますforce-staticがcookiesとheadersを空にする:dynamic = 'force-static'は、cookiesやheaders()を空の値で返させて静的にします。ユーザー別の表示が消える原因になりますrevalidate = 60 * 10のような式: ルートセグメントのrevalidateは静的に解析できる値でなければならず、600と書く必要があります
開発サーバーでは、ページは常にオンデマンドで描画されキャッシュされません。再現しないときはnext devではなく、後で触れるnext buildとnext startの組み合わせで確認します。
効かなさすぎて毎回取得される
revalidate: 0やcache: 'no-store': どちらかが1つでもあると、ルートは動的に描画されます。dynamic = 'force-dynamic'は、ルート内のすべてのfetchを{ cache: 'no-store', next: { revalidate: 0 } }にするのと同じです- リクエスト時APIの後ろにある
fetch: 既定(fetchCache = 'auto')では、cookies()などを使った後に出てくるfetchはキャッシュされません - 矛盾するオプション:
{ revalidate: 3600, cache: 'no-store' }のように両立しない指定は、どちらも無視されます。開発中は警告がターミナルに出ます
Claude Codeに直させるときは、「記号をƒから○へ戻したい」「キャッシュ対象をgetProductsに限る」のように、期待するビルド出力とスコープを文で固定します。dynamicの設定はlayout、page、routeのどれにも書けるので、書いた場所が広すぎるとほかのルートまで巻き込みます。
Cache Componentsで古いデータが出る型
cacheComponentsを有効にしている場合は、use cacheが付いた関数やコンポーネントを疑います。
cacheLifeを書かないと既定のプロファイルになる:use cacheの既定は、クライアント側のstaleが5分、サーバー側のrevalidateが15分、expireは時間では切れません。「更新が15分遅れる」のは、この既定値のままという可能性があります- プロファイルの選び違い:
hoursはrevalidateが1時間、daysは1日です。更新頻度に合わない名前を選ぶと、古さがそのまま見えます - 短すぎる設定はキャッシュから外れる:
secondsプロファイル、revalidate: 0、expireが5分未満のものは短命とみなされ、プリレンダリングから除外されて動的な部分になります。キャッシュしたつもりで効いていない例です - キャッシュキーに入るもの: ビルドID、関数ID、シリアライズできる引数が入ります。外側のスコープの変数も引数として束縛されるため、ユーザーIDなどを渡すと別エントリになります
cookies()やheaders()は中で呼べない: 外で読んで引数として渡します。渡す値が細かすぎると、エントリが分かれてヒットしません
実行環境でも結果が変わります。サーバーレスでは、リクエストごとに別のインスタンスになり得るため、ランタイムのキャッシュエントリは通常残りません。セルフホストではエントリが残り、サイズはcacheMaxMemorySizeで制御します。
ビルドが固まる症状もあります。Filling a cache during prerender timed outというエラーが約50秒後に出るなら、use cacheの中でリクエスト固有の値(params、searchParams、cookies()、キャッシュされないデータ)を使っています。Promiseのまま渡す、クロージャで参照する、共有のMapから取り出す、といった形で起きます。
無効化したのに変わらないとき
キャッシュが効いていること自体は正しく、更新の届き方で食い違うこともあります。タグの無効化は、関数によって挙動が違います。
updateTag | revalidateTag | |
|---|---|---|
| 呼べる場所 | updateTagServer Actionのみ | revalidateTagServer ActionとRoute Handler |
| 挙動 | updateTagすぐに期限切れにする | revalidateTagstale-while-revalidate |
| 向く場面 | updateTag書いた直後に自分の変更を見せる | revalidateTag多少遅れてよいバックグラウンド更新 |
revalidateTag('posts', 'max')は、古い内容を返しながら裏で新しい内容を取りに行きます。呼んだ直後の最初の訪問で古い内容が見えるのは、この仕様どおりです。管理画面で投稿した直後に自分の変更を見せたいときは、Server ActionからupdateTagを使います。
次のような見落としもあります。
- タグの綴りが違う:
cacheTag('products')とrevalidateTag('product')のように、文字列が一致していません。Claude Codeに2つをgrepで突き合わせさせると見つかります revalidatePathの指定パス: Proxyは、オンデマンドISRのリクエストでは実行されません。書き換え後のパス(/post-1)ではなく、実際のルート(/post/1)を指定します- 複数インスタンス: 既定のファイルシステムキャッシュはインスタンスごとにあり、無効化は呼び出しを受けたインスタンスにしか届きません。共有のキャッシュハンドラーで揃えます
サーバーは新しいのに画面が古いとき
サーバー側を直しても、ブラウザのクライアントキャッシュに古い内容が残っていることがあります。クライアントキャッシュはブラウザのメモリ上に、訪問済みやプリフェッチ済みのルートのRSCペイロードを持ちます。ページの更新で消え、revalidateTag、revalidatePath、updateTag、router.refresh、cookies.set、cookies.deleteでも無効化されます。
use cacheの内容は、staleの時間だけブラウザのメモリに残ります。クライアントのルーターは、設定にかかわらず最低30秒のstale時間を適用します。サーバーが新しくなってから数十秒は古い表示が続き得る、ということです。実験的なstaleTimesのdynamic既定は、v15.0.0で30秒から0秒に変わりました。
切り分けの順番
ここまでの確認を、手を動かす順に並べます。
古いデータの切り分け
- Qnext buildとnext startでも古いデータが再現するか?答えが再現しない答えがはい
開発サーバーはページを毎回描画するため、本番と挙動が違います。本番相当の起動で確かめ直します。
- Q該当ルートはビルド出力で静的(○または◐)か?答えが動的(ƒ)答えがはい
キャッシュが効いていません。no-storeやrevalidate: 0、リクエスト時APIより前に取得が置かれているかを確認します。
- Q更新のあとに無効化を呼んでいるか?答えが呼んでいない答えがはい
Server ActionにupdateTagかrevalidateTagを足し、タグ名をcacheTagと一致させます。
サーバーが新しければクライアントキャッシュ
router.refreshやstale時間の設定を確認します。複数インスタンスなら共有キャッシュも見ます。
Claude Codeに渡す材料と、直した後の確認
Claude Codeには、症状・再現コマンド・ビルド出力の3点を最初に渡します。common-workflowsの不具合修正の項では、再現コマンドと再現手順、再現が毎回かどうかを伝えることが勧められています。
挙動の確認には、次のコマンドが使えます。
npm run build
NEXT_PRIVATE_DEBUG_CACHE=1 npm run start
curl -sI http://localhost:3000/products | grep -i x-nextjs-cacheNEXT_PRIVATE_DEBUG_CACHE=1を付けると、キャッシュのヒットとミスがサーバーのログに出ます。x-nextjs-cacheヘッダーはHIT、STALE、MISS、REVALIDATEDの値を取ります。更新の直後にSTALEが返るなら、バックグラウンドで再生成している途中です。
fetchのどれが取得されたかを見たいときは、next.config.jsでlogging.fetches.fullUrlをtrueにします。
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
}依頼は、調べる段階と直す段階に分けると安全です。次のような形が使えます(例示であり、実際の応答は環境によって変わります)。
/products の古いデータを調べてください。
1. next.config の cacheComponents の有無を確認
2. getProducts の取得方法と cacheLife / revalidate の値を列挙
3. 更新用の Server Action が呼ぶ無効化関数とタグ名を列挙
修正案は表で出し、承認するまでコードは変えないでください。直した後は、同じコマンドでnext buildをやり直し、ルートの記号が変更前と変わったかを比べさせます。ƒが○に戻っただけでは更新が届くことの証明にならないので、curlのx-nextjs-cacheと、更新後のレスポンスの中身まで確かめます。
毎回貼り直すのが手間なら、プロジェクトのCLAUDE.mdに前提を書いておきます。CLAUDE.mdは、セッションの開始時にClaudeが読むプロジェクト指示のファイルです。
## Next.js のキャッシュ
- cacheComponents は有効(next.config.ts を確認)
- 新しいキャッシュは `use cache` + `cacheLife` + `cacheTag` で書く
- unstable_cache と fetch の `next.revalidate` は新規に使わない
- 無効化は Server Action 内の `updateTag`(即時)か `revalidateTag(tag, 'max')`
- キャッシュ変更後は `npm run build` の出力の記号を報告するcacheComponentsを使わないプロジェクトでは、2行目以降を従来モデルの規約に置き換えます。移行の途中でpagesからappへ書き換えている場合は、Claude CodeでApp Routerへ移行するで、getStaticPropsのrevalidateがfetchのオプションに対応する形を整理しています。Next.jsの実行状態をClaude Codeから参照する設定は、Next.js MCPサーバーをClaude Codeで使う設定ガイドにあります。
まとめ
古いデータの原因は、ビルド時の静的化、revalidateやcacheLifeの値、無効化の届き方、クライアントキャッシュの4層のどれかに収まります。next buildの記号で層を決め、next startで再現し、x-nextjs-cacheで確かめる順に進めると、Claude Codeへの指示も「この層のこの値を直す」と具体的に書けます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- nextjs.org/docs/app/getting-started/caching
- nextjs.org/docs/app/getting-started/revalidating
- nextjs.org/docs/app/guides/caching-without-cache-components
- nextjs.org/docs/app/api-reference/directives/use-cache
- nextjs.org/docs/app/api-reference/functions/fetch
- nextjs.org/docs/app/guides/incremental-static-regeneration
- code.claude.com/docs/en/common-workflows
- code.claude.com/docs/en/memory