0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React/Vite】デプロイ跨ぎで画面が白く落ちる「ChunkLoadError」を完全撲滅する ― safeLazyと自動復旧パイプライン設計

0
Last updated at Posted at 2026-09-24

SPA(Single Page Application)を本番運用していて、こんな障害報告を受けたことはないでしょうか。

「さっきまで普通に使えていたのに、別のタブや設定画面を開いたら突然画面が真っ白になりました」

原因を調べてみると、コンソールに残っているのはお馴染みのエラーです。

TypeError: Failed to fetch dynamically imported module: https://example.com/assets/SettingsModal-B4Fll2ZS.js

いわゆる ChunkLoadError(チャンクロードエラー) です。

Vite や Webpack でコード分割(Code Splitting)を行っている場合、デプロイのたびにアセットファイル名にハッシュ(-B4Fll2ZS.js など)が付与されます。ユーザーが画面を開いたままの状態で新しいバージョンがデプロイされると、古い HTML が参照している過去のハッシュ付きチャンクが配信サーバー(Vercel, Cloudflare, S3等)から消滅し、404 になって画面がクラッシュする のです。

筆者が開発しているタスク管理SaaS「Taski」でも、初期ロードを高速化するために20以上のモーダルや重厚なタブ(ガントチャート・カンバン・設定)を遅延ロード(React.lazy)に切り替えた直後、この問題に直面しました。

この記事では、単に window.location.reload() を呼ぶだけでは防げない「白い画面の一瞬のチラつき」「無限リロードループ」「Sentryのエラークォータ浪費」までを完全に防ぐ、4重の自動復旧パイプライン設計 を解説します。


1. なぜ「ChunkLoadError」は起きるのか?

まずは問題のメカニズムを整理します。

ユーザーがリロードすれば最新の index.html(v2)が読み直されて解決するのですが、ユーザーにとっては**「突然アプリが壊れた」**ようにしか見えません。


2. 単純な対策が引き起こす「3つの罠」

「エラーが起きたらリロードすればいいのでは?」と考えるのが自然ですが、素朴な実装には落とし穴があります。

罠①:リロードが完了するまで「白い画面」や「エラー境界」が一瞬表示される

動的インポートが rejected になると、React の Suspense や ErrorBoundary にエラーが伝播します。リロードを発火してもページ遷移には数百ミリ秒かかるため、その間ユーザーにクラッシュ画面を見せてしまいます。

罠②:ネットワーク切断時やCDN不調時の「無限リロードループ」

本当に回線が切れていたりCDNがダウンしている場合、リロードしても再び 404 になります。対策なしに window.location.reload() を呼ぶと、ブラウザが狂ったように無限リロードを繰り返し、ユーザーが操作不能 に陥ります。

罠③:Sentry などのエラー監視ツールが過渡的エラーで埋まる

デプロイ跨ぎのエラーは自動復旧できる「正常な過渡現象」です。しかし素朴に放置すると、デプロイのたびに数十〜数百件の ChunkLoadError が Sentry に飛んでアラートが鳴り響き、月間のエラー送信クォータを食い潰します。


3. 実践した「4重の自動復旧パイプライン」

Taski では、これらすべての課題を解決するために以下の4段階のアーキテクチャを構築しました。


4. コード解説

① エラーを握りつぶして白い画面を防ぐ safeLazy

通常の React.lazy(() => import('./Component')) は、Promise が reject されると上位の ErrorBoundary へ例外をスローします。

そこで、動的インポートの失敗時にエラーをキャッチし、空のダミーコンポーネント(() => null)を返すラッパー関数 safeLazy を用意します。

// src/App.tsx

/**
 * デプロイ跨ぎによるチャンクロードエラー時にも TypeError を投げず、
 * vite:preloadError による window.location.reload() を安全に待機するための safeLazy
 */
function safeLazy<M, T extends React.ComponentType<any>>(
  loader: () => Promise<M>,
  selector: (m: M) => T
): React.LazyExoticComponent<T> {
  return React.lazy(() =>
    loader()
      .then((m) => {
        const Comp = m ? selector(m) : null;
        return { default: Comp || ((() => null) as unknown as T) };
      })
      .catch((err) => {
        // エラーをコンソールに残しつつ、例外をスローしない
        console.warn('Chunk load error, waiting for auto-reload:', err);
        return { default: (() => null) as unknown as T };
      })
  );
}

// 使い方
const SettingsModal = safeLazy(
  () => import('./components/SettingsModal'),
  (m) => m.SettingsModal
);
const GanttChart = safeLazy(
  () => import('./components/GanttChart'),
  (m) => m.GanttChart
);

ポイント

safeLazy が () => null を返すことで、React の描画ツリーは壊れません。後述の window.location.reload() が完了するまでのコンマ数秒間、画面が真っ白になることなく現在の画面をキープしたまま、最新版へとスムーズにリロードされます。


② Vite公式イベントとブラウザ標準エラーの二重検知

Vite には、動的インポートの失敗を検知するための専用カスタムイベント vite:preloadError が用意されています。

これに加え、Safari や一部の古い環境でも取りこぼさないよう、グローバルの error イベントでもフォールバック検知を行います。

// src/main.tsx

let isReloadingForChunk = false;

// 1. Vite 公式のプリロードエラーイベント
window.addEventListener('vite:preloadError', (event) => {
  isReloadingForChunk = true;
  event.preventDefault(); // デフォルトのエラーログ出力を抑制
  triggerSafeReload();
});

// 2. ブラウザ標準のエラーイベント(フォールバック)
window.addEventListener('error', (event) => {
  const msg = event?.message || '';
  if (
    msg.includes('Failed to fetch dynamically imported module') ||
    msg.includes('Importing a module script failed') ||
    msg.includes('error loading dynamically imported module')
  ) {
    isReloadingForChunk = true;
    triggerSafeReload();
  }
});

③ 無限リロードを防ぐ「10秒クールダウン」設計

ネットワークが物理的に切断されている場合にブラウザがリロードループに陥らないよう、sessionStorage を使って**「直近10秒以内にすでにリロードしたか」**をチェックします。

// src/main.tsx

function triggerSafeReload() {
  const reloadKey = 'taski_chunk_reload_retry';
  const lastReload = sessionStorage.getItem(reloadKey);
  const now = Date.now();

  // 10秒以内にすでにリロードされていたらループとみなして中断
  if (lastReload && now - parseInt(lastReload, 10) < 10000) {
    console.error('Chunk reload loop detected. Stopping automatic reload.');
    return;
  }

  sessionStorage.setItem(reloadKey, now.toString());
  window.location.reload();
}

ユーザーが1回アクセスしたデプロイ跨ぎなら、1回目のリロードで最新の HTML とチャンクを取得して即座に解決します。もし万が一サーバー障害等で404が続いても、10秒間はリロードがブロックされるため、ブラウザのフリーズを防ぐことができます。


④ Sentry のエラー送信を安全にインターセプト

自動復旧中の過渡的な ChunkLoadError が Sentry に飛んでアラートを汚染しないよう、Sentry の beforeSend フックで除外します。

// src/main.tsx
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  beforeSend(event) {
    // チャンクロード失敗による自動リロード中の過渡的エラーは Sentry 送信を抑止
    if (isReloadingForChunk) {
      return null;
    }

    // ErrorBoundary等でラップされた例外も確実に検知して除外
    const isChunkError = event.exception?.values?.some((val) => {
      const text = `${val.type || ''} ${val.value || ''}`;
      return (
        text.includes('Failed to fetch dynamically imported module') ||
        text.includes('Importing a module script failed') ||
        text.includes('error loading dynamically imported module')
      );
    });

    if (isChunkError) {
      return null;
    }

    return event;
  },
});

フラグ isReloadingForChunk と例外メッセージの両方をチェックすることで、Sentry に1件も不要なエラーを流すことなく、本当のバグだけに集中できるようになります。


5. 導入前後の比較

項目 対策前(素の React.lazy) 対策後(本パイプライン)
デプロイ跨ぎの体験 画面が真っ白にクラッシュし、手動リロードが必要 一瞬のチラつきもなく自動で最新画面に復帰
オフライン・障害時 無限リロードでタブがフリーズするリスク 10秒のセーフティガードで安全に停止
Sentry エラー通知 デプロイごとに ChunkLoadError のアラートが大量発生 過渡的エラーが完全にゼロ になりノイズ撲滅
初期表示速度(LCP) バンドルサイズ 1.8MB のまま分割できない 初期バンドルを極小化 しつつ安全にコード分割

まとめ

SPA のコード分割(Code Splitting)はページの初期表示速度を劇的に改善する強力な武器ですが、「デプロイ跨ぎ」という運用上の壁が必ず立ちはだかります。

今回実装したパイプラインの肝は以下の4点です:

  1. safeLazy: エラーを握りつぶしてダミーコンポーネントを返し、白い画面を防止する
  2. vite:preloadError: 公式イベントで最短でリロードをキックする
  3. sessionStorage: 10秒以内の連続リロードを遮断し、無限ループを防ぐ
  4. beforeSend: リロード中の過渡的エラーを Sentry から除外してクォータを守る

Vite で SPA を運用している方は、ぜひ取り入れてみてください。


プロダクトのご紹介

この記事で解説した「safeLazy」と自動復旧パイプラインは、筆者が開発しているプロジェクト・タスク管理ツール 「Taski」 で実際に稼働しています。

ガントチャートやカンバン、詳細モーダルをすべて遅延ロードしながら、高速性とデプロイ無停止を両立しています。会員登録不要・1秒でデモ画面を体験できます ので、SPAのサクサク感をぜひ触ってみてください!

👉 Taski(タスキ)のデモ画面を試してみる(登録不要・1秒起動)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?