次のスクリプトは Node 22 で動かして確かめています。ステータスだけでなく content-type も見ているのは、200 で HTML が返ってくる型の失敗があるためです。
// check-chunks.mjs — デプロイ後に、HTML が呼ぶ JS が全部実在するかを確かめる// 使い方: node check-chunks.mjs https://example.com/ https://example.com/en/const urls = process.argv.slice(2);if (urls.length === 0) { console.error('usage: node check-chunks.mjs <page-url> [...]'); process.exit(2);}let failed = 0;for (const page of urls) { const res = await fetch(page, { headers: { 'Cache-Control': 'no-cache' } }); const html = await res.text(); const found = new Set( [...html.matchAll(/(?:src|href)="([^"]*\/_next\/static\/[^"]+\.js[^"]*)"/g)].map((m) => new URL(m[1], page).href), ); console.log(`${page} -> HTTP ${res.status}, x-cache=${res.headers.get('x-cache') ?? '-'}, chunks=${found.size}`); for (const chunk of found) { const r = await fetch(chunk); const type = r.headers.get('content-type') ?? ''; await r.arrayBuffer(); const ok = r.status === 200 && /javascript/.test(type); if (!ok) { failed += 1; console.log(` NG ${r.status} ${type || '(no content-type)'} ${chunk}`); } }}console.log(failed === 0 ? 'OK: すべてのチャンクが JS として返りました' : `FAIL: ${failed} 件のチャンクが読めません`);process.exit(failed === 0 ? 0 : 1);
手元で試すために、チャンクを三本呼ぶ HTML を返すだけのローカルサーバーを立てました。一本は 404、もう一本は 200 で HTML を返すように細工しています。実際の出力は次のとおりです。
http://127.0.0.1:36245/ -> HTTP 200, x-cache=-, chunks=3 NG 404 text/html http://127.0.0.1:36245/_next/static/chunks/b.js NG 200 text/html http://127.0.0.1:36245/_next/static/chunks/c.jsFAIL: 2 件のチャンクが読めません
終了コードが 1 になるので、デプロイの最後の工程に置けば、壊れたまま気づかずに終わる事態を避けられます。なお、この検査が見ているのは、取りに行った時点で配られているものです。エッジの古い HTML を拾いたいときは、同じページを二回取って、二回目の x-cache が HIT になるのを確かめてから流します。
手当て2:キャッシュの HTML に、作ったビルドの版を持たせる
次に、エッジの古い HTML を、再デプロイでは消えない問題として扱いました。やることは単純で、保存するときにビルドの版を一緒に持たせ、返す前に照合します。版が違えば捨てて、作り直します。
判定関数は、次の場合を確かめました。ChunkLoadError と __name is not defined はチャンクの失敗として判定されること。無関係な TypeError は判定されないこと。一回目は再読み込みを許し、三十秒後は許さず、七十秒後は再び許すこと。そして sessionStorage が例外を投げる環境では、再読み込みしないこと。すべて通りました。
落とし穴:直したつもりが、別の事故を呼ぶところ
この三つを入れる過程で、私が気をつけた箇所は四つあります。
自動再読み込みの無限ループ
再読み込みしても、まだ古い HTML が返れば、また同じエラーになり、また再読み込みします。時刻を保存して一分以内は再読み込みしないのは、そのためです。ここを省くと、利用者の画面が点滅し続けます。
200 で HTML が返る型の失敗は、ステータスでは見分けられません。content-type まで見るのは、そのためです。
設計の線引き:古いチャンクを残す道は、選びませんでした
別の対処として、新しいビルドでも古いチャンクを数世代残しておく道があります。そうすれば、古い HTML を持っている人も、そのまま動かせます。
私はこの道を選びませんでした。配信するファイルが増えて、何世代残すかを管理する手間が増えるからです。それに、古い HTML を配り続けること自体が、本来は避けたい状態でした。ずれを許して残すより、ずれを見つけて、速やかに入れ替えるほうを選んでいます。 ただし、利用者が多く、デプロイの頻度も高いサイトでは、残す道のほうが合うかもしれません。ここは、お持ちのサイトの事情に合わせて選んでいただければと思います。
最後に、次の一手を一つだけ
もし今日、一つだけ試していただけるなら、手当て1の検知スクリプトを、デプロイの最後の工程に置いてみてください。数分で入りますし、ここで一本でも NG が出れば、利用者より先に自分が気づけます。