この記事は、Zennに掲載した記事の転載です。原本はこちらです。
https://zenn.dev/yukina200/articles/nextjs-cache-components-errors-12
はじめに
Next.js 16.4で推奨となったCache Components。既存アプリで cacheComponents: true を有効化したら、どんなエラーが、どの順番で出るのか。検索しても「大量のエラーが出た」という感想はあっても、エラー文と対処の時系列記録はあまり見かけません。
そこで、実務寄りの題材アプリをNext 15.5.28(React 19.2.9)から16.4.4へ移行し、出たエラーをすべて記録しました。結論を先に書くと、エラーは合計12件+警告1件。ただし大半は同型の削除作業で、本当に判断が必要だったのは数件だけでした。一番の特徴は「エラーが1件ずつしか見えない」ことです。この記事の主眼は、時系列の再現そのものではなく切り分けです。手元で同じエラーに遭遇したときに「エラー文のどこを見て、最初に何を確認するか」を、実際に出た12件を材料に整理します。時系列の全記録は、書籍の第3章に譲ります。
題材アプリ
移行対象は小さなEC風アプリです。認証(cookieのフェイクセッション)、SQLiteに持つ商品200件・レビュー80件、商品一覧と詳細(SSG 200ページ)、検索、特価ページ、レビュー投稿などのServer Actionを持ちます。キャッシュまわりは移行対象として意図的に旧式の書き方を混ぜてあります。
-
unstable_cache(revalidateとtags付き) - ルートセグメント設定の
revalidate/fetchCache/dynamic = "force-dynamic" - ページ先頭での
cookies()読みとsearchParams読み
エラーの時系列(凝縮版)
0. フラグの前:更新だけで型エラー3件
16.4.4に上げた最初のビルドで、Cache Componentsと無関係の場所が落ちました。
lib/actions.ts(20,3): error TS2554: Expected 2 arguments, but got 1.
lib/actions.ts(35,3): error TS2554: Expected 2 arguments, but got 1.
lib/actions.ts(36,3): error TS2554: Expected 2 arguments, but got 1.
Failed to type check.
Next 16では、revalidateTag(tag) の単一引数形が非推奨になり、現在のTypeScript定義では第2引数が必要になったため、このプロジェクトではTS2554になりました。移行はフラグを立てる前から始まっていました。ここでは型エラーを解消する暫定形として "max" を追加し、先に進みました。ただし、"max" は旧単一引数と等価ではありません。旧形は { expire: 0 } と同じく古い内容を返さない挙動として非推奨化されており、"max" は再検証中に古い内容を返せる窓を1年にする指定です。あわせてPlaywrightでレビュー投稿・在庫変更・ログイン・ログアウトのE2Eを移行前にグリーン(4/4)にしておき、「移行で壊したかどうか」を切り分けられる状態にしました。
1. フラグ有効化:警告1件+設定エラー6件
cacheComponents: true を追加したビルドでは、まず partialPrefetching を指定せよという警告が1件出て、続いて6件のエラーで止まりました。全て同型です。
Error: Route segment config "dynamic" is not compatible with `nextConfig.cacheComponents`. Please remove it.
Error: Route segment config "fetchCache" is not compatible with `nextConfig.cacheComponents`. Please remove it.
Error: Route segment config "revalidate" is not compatible with `nextConfig.cacheComponents`. Please remove it.
対象は dynamic、fetchCache、revalidate の3種類の設定で、4ファイルに散らばっていました。この種類は場所も理由も明示されるので、対処は「全部削除する」だけです。意味の置き換え(寿命の3値とタグは、データ側の cacheLife / cacheTag へ)は後のフェーズに分け、ここでは削除だけ行いました。
2. プリレンダリングエラー:ここから1件ずつ
設定を消すと、ビルドは静的生成の途中で止まるようになります。ここからが本番で、エラーは1件ずつしか出ません。
1件目は /deals。force-dynamic を消したことでプリレンダリング対象になり、new Date() が静的シェルをブロックしました。
Error: Route "/deals": Next.js encountered the unstable value `new Date()` while prerendering.
同じページにはランダム抽選の処理もありましたが、報告されるのは最初の1件だけです。対処はエラーメッセージの案内どおりで、動的部分を <Suspense> 内の別コンポーネントに分け、先頭で await connection() を呼んでリクエスト時レンダリングを明示しました。
2件目は /account。ページ先頭の cookies() 読みが対象です。
Error: Route "/account": Next.js encountered uncached or runtime data during prerendering.
`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.
Next 15では「ルート全体がDynamicになる」だけだった読み方が、Cache Componentsではビルドエラーになります。スタックトレースは匿名で、行番号は出ないため、ルート名とメッセージの列挙を手がかりに、セッション読みを <Suspense> 内の子コンポーネントへ移動しました。
3件目は /search の searchParams で、同型のエラーでした。ページは searchParams のPromiseをそのまま子コンポーネントへ渡し、Suspenseの内側で初めて await する形に直して、ビルドが通りました。
興味深いのは、この時点でデータ層の unstable_cache はそのままだったことです。公式ガイドどおり、unstable_cache はCache Componentsでも動き続け、移行は任意でした。「フラグを立てたら全部書き換えるまで通らない」ということはありません。
切り分けの型
この経験から、移行時の切り分けは次の順序が効くと分かりました。前提として、エラー文は「最初に見る箇所」まで教えてくれています。設定エラーはファイルと行を、プリレンダリングエラーはルート名と対象APIの列挙を、型エラーは呼び出し箇所を示します。逆に、プリレンダリングエラーのスタックトレースは行番号を出してくれないので、「ルート名から、そのルートのランタイムAPI使用箇所を洗い出す」が手順になります。
- フラグの前に、バージョンアップだけのエラーを潰す。 型エラーはCache Componentsと切り分ける。移行前のE2Eもここでグリーンにしておく。
- 設定エラーは一括で削除だけする。 意味の置き換えを同じコミットに混ぜない。
-
プリレンダリングエラーは「1件直して再ビルド」の反復と割り切る。 ルート名を確認し、そのルートで使っているランタイムAPIや不安定な値(
new Date()など)を洗い出して、「Suspenseへ切り出す」か「connection()でリクエスト時実行を明示する」かで解消する。次が出るのは仕様どおりの進行です。 -
ビルドが通ったらコミットし、データ層の移行は別フェーズにする。
use cache化や無効化の設計は、通った状態を確保してから。
なお、計測面の正直な注記もしておきます。移行直後の観測では、ビルド時間は27.9秒から16.7秒へ、検索ページのTTFB(応答開始までの時間)は14.5msから4.7msへ変化しました。ただしビルドの2数字は計測条件が揃っていません(移行前は初回・単回実行、移行後は温まった状態での3回実行の中央値)。その後の統制比較(Next 16.4.4内・クリーンビルド・各3回)では、短縮の主因はNext 16のTurbopackデフォルト化(約13秒)で、Cache Components自体はビルドを1.5〜4.3秒むしろ遅くすることが分かりました。TTFBも、フラグ有無の統制比較では5.0ms対15.1msと同方向でした。計測の詳細は、計測を扱った別記事と書籍の第10章にまとめています。
この先の話(書籍のお知らせ)
書籍『Next.js 15から16へ Cache Components移行実践|12件のエラーと検証・切り戻し』(Zenn Books、2,500円)を公開しました。第1・2・3・6章は無料で読めます。
https://zenn.dev/yukina200/books/nextjs-cache-components-migration
この記事は、移行作業のうち「エラーが出て、通るまで」の部分だけを、切り分けの観点で切り出したものです。書籍では、この続きとして次の内容までを、同じアプリ・同じ手順で扱います。
- エラー12件の時系列の全記録(直すたびに次に何が表面化するか)
- ルート単位の置き換え手順(
revalidate/fetchCache/dynamicをuse cache/cacheLife/cacheTagへ) -
cacheTagとupdateTagによるオンデマンド無効化の設計と事故パターン - 移行前後の全計測(build時間・TTFBを、統制条件での切り分けまで含めて)
- Vitest + Playwrightでキャッシュの退行を捕まえるためのガードレール
- ロールバックと段階公開の手順(データ層だけを戻す中間状態を含む)
「フラグを立てたらエラーが出て、一度元に戻した」という状態なら、戻したところから再開できるはずです。その再開の手順書として書いています。