💥

あなたはVersion Skew問題を知っていますか? Web開発者なら知って損はない原因と対策

に公開1

Webサイトのリリース後に 「一部のユーザーだけ画面が真っ白になる」「謎のエラーが飛んでくる」 といった現象に遭遇したことはありませんか?
もしかしたら、それは Version Skew(バージョンスキュー) と呼ばれる問題のせいかもしれません。

最初に

こんにちは。ココナラテックの開発をしているエンジニアのもちさんです。

私はある日、手元の端末で再現性の低いエラーに悩まされました。
ブラウザキャッシュを消すと直る、でも根本原因がわからない。そんな厄介な症状の裏に、この「新旧リソースの混在」という構造的な問題が潜んでいました。

この記事では、実際に起きたトラブルの経緯をもとに、Version Skewとは何か/なぜ起こるのか/どう防げるのかを整理していきます。

「うちの環境では起きてないから関係ない」 と思っている方ほど、読んでおくと安心です。

簡単に言うと(1分で把握)

  • Version Skew画面(HTML)と参照するリソース(JavaScript/CSS)の世代が違うなど、新旧のリソースが混ざってバージョン不整合による歪み(Skew)が発生している状態のこと[1]
  • ありがちな例:デプロイ直後、 ブラウザのキャッシュ上に古いJavaScriptが残っておりエラーが出る。
  • Next.js on Vercelなら Skew Protection をONにすると、同じデプロイIDのサーバーへ自動で振り分けて混在を防げる[2]
  • 自前ホスティングでも、「HTMLは短くキャッシュ」「JavaScript/CSSはハッシュ付きで長くキャッシュ」「APIをバージョニングする」 等の対応で多くの不具合を避けられる[1:1][3][4]

今回のトラブル実例

  • 症状: Next.jsで運営しているサイトを手元の端末で開くと画面遷移時に ChunkLoadError が発生する。
  • 調査: ChromeのNetwork タブでは HTMLは旧ビルドだが /_next/static/... の一部が新ビルドを参照している状態。
  • 原因: 古いHTML × 新しいJavaScript の組み合わせで発生することが判明(Version Skew)。
  • 即応: Vercel の Skew Protection をONに切替(古いプロジェクトのためデフォルトで有効化されていなかった)。

調査で苦労したこと(なぜ時間がかかったか)

  • 再現性が低い: ブラウザーのキャッシュが残った状態で再訪問タブの開きっぱなしなど環境依存で、手元で再現するのが難しかった。
  • Sentry 側の除外設定: SentryによるInbound FiltersによってChunkLoadError を除外しており、痕跡がログに残りづらかった

どこでも起こり得る「身近な」ケース

よくある例

  1. 画面Aの古いJavaScriptをユーザーのブラウザが自動でキャッシュしている。
  2. リリースして 画面B(新しいHTML) に切り替え。
  3. 一部の人のブラウザは古いJavaScriptをそのまま使い新しいHTML古いJavaScriptミスマッチ
  4. バージョンの不整合でエラーが発生する。

図1:古いJavaScript × 新しいHTML → 食い違い

対応策

Next.js on Vercel

  • プロジェクト設定のSkew Protection をON
  • 旧バージョン(13.4.7〜14.1.3)のNext.jsなら next.config.js に以下を追加:
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    useDeploymentId: true,
    // Server Actions も同じデプロイに固定したい場合
    useDeploymentIdServerActions: true,
  },
};
module.exports = nextConfig;

これでブラウザが見ているデプロイIDに合わせて同じ世代のサーバーへ誘導され、混在しづらくなります[2:1]

自前/他プラットフォーム

  • フレームワークやプラットフォームにVersion Skewの防止機能がある場合はそれを用いる
  • HTMLは短めにキャッシュno-store or 短TTL)。
  • JavaScript/CSSはファイル名にハッシュを付け、長くキャッシュするimmutable[4:1]
  • 同時に複数台で配信するなら、ビルド番号(Build ID)を全台で合わせる[6]

Next.jsでの対策例

next.config.js
module.exports = {
  generateBuildId: async () => {
    // 例:GitのコミットIDを使う
    return process.env.GIT_HASH
  },
}

最後に

Version Skewは「デプロイ直後にしか起きないレアケース」と思われがちですが、
キャッシュ戦略の不整合や複数デプロイの並行運用など、 どんなプロダクトにも潜みうる「構造的リスク」です。

特にNext.jsのようなフレームワークはHTML・JavaScript・APIの生成タイミングが複雑なため、
「新旧の整合性をどう担保するか」を意識していないと、思わぬところで食い違いが発生します。

幸い、Vercelなどの環境では Skew Protection が強力にサポートされており、自前環境でも キャッシュ・バージョニング・Build IDの固定 などの原則で十分に緩和可能です。

この記事が、あなたのプロダクトを「静かに潜むバージョン不整合」から守るヒントになれば幸いです。


ココナラではエンジニアを絶賛募集中です。
少しでも興味を持たれた方がいましたら、ぜひ下記の採用ページをご覧ください。

  • 採用情報

https://coconala.co.jp/recruit/

  • カジュアル面談をご希望の方

https://open.talentio.com/r/1/c/coconala/pages/72880

参考リンク

  • Version Skew の定義と戦略:Malte Ubl, Version Skew[1:2]
  • Vercel Skew Protection(Next.js 14.1.4+は設定不要)[2:2]
  • Next.js Self-Hosting(キャッシュ既定・自動緩和)[3:1]
  • immutableの正式仕様:RFC 8246[4:2]
  • アトミック/イミュータブルデプロイ:Netlifyブログ[7]
  • ファイル名バージョニング:CloudFrontドキュメント[8]
  • Build ID固定generateBuildId[6:1]
脚注
  1. Industrial Empathy, Version Skew(定義・境界・戦略)https://www.industrialempathy.com/posts/version-skew/ ↩︎ ↩︎ ↩︎

  2. Vercel Docs, Skew Protection(Next.js 14.1.4+はnext.config.js(ts)に追加の設定の必要なし、x-deployment-id/__vdpl/dpl など)。https://vercel.com/docs/skew-protection ↩︎ ↩︎ ↩︎

  3. Next.js Docs, Self-Hosting(Version Skewの自動リロード、Cache-Control既定)。https://nextjs.org/docs/app/guides/self-hosting ↩︎ ↩︎

  4. RFC 8246, HTTP Immutable ResponsesCache-Control: immutable)。https://datatracker.ietf.org/doc/html/rfc8246 ↩︎ ↩︎ ↩︎

  5. 同記事の Frontend client skew の小節に「ブラウザのタブを長期間開いたまま」「ネイティブモバイルアプリの自動更新OFF」の例示あり。 ↩︎

  6. Next.js Docs, next.config.js: generateBuildId(Build ID を自前で固定)。https://nextjs.org/docs/app/api-reference/config/next-config-js/generateBuildId ↩︎ ↩︎

  7. Netlify Blog, Atomic and immutable deployshttps://www.netlify.com/blog/2021/02/23/terminology-explained-atomic-and-immutable-deploys/ ↩︎

  8. AWS CloudFront Docs, Use file versioning to update or remove contenthttps://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/UpdatingExistingObjects.html ↩︎

Discussion