シナリオ:コード分割がUIを壊すとき最近、メインのJavaScriptバンドルが2.5MBを超えて肥大化したダッシュボードを監査しました。軽量化のために、重いデータグリッドやD3チャートをオンデマンドの小さなチャンクに分割しようと、React.lazy()を採用しました。ローカル開発では軽快に動作していました。しかし、ステージング環境にデプロイして「Analytics」タブをクリックした瞬間、アプリケーション全体が消えてしまいました。代わりに表示されたのは真っ白な画面と、ブラウザコンソールに出力された大きなエラーでした。
Error: A React component suspended while rendering, but no fallback UI was specified.
このクラッシュは、Reactがまだ存在しないコンポーネントをレンダリングしようとしたために発生します。JSファイルがネットワーク経由でダウンロード中だったため、Reactは処理を継続できなくなりました。その数ミリ秒の空白時間に何を表示すべきか、Reactに指示が与えられていなかったのです。
なぜReactはこのエラーをスローするのかReactのエコシステムにおいて、「サスペンド(中断)」は一つのシグナルです。コンポーネントはレンダラーに対して、「ネットワークレスポンスや遅延ロードされたチャンクなどのリソースを待っているため、まだ準備ができていない」と伝えます。
React.lazy()を使用すると、動的インポート(dynamic import)が作成されます。そのファイルのダウンロードには、特に3G回線などでは時間がかかります。もし遅延ロードされるコンポーネントを<Suspense>境界で囲まなかった場合、Reactは不完全な、あるいは壊れたUIを表示することを拒否します。代わりに、状態の不整合を防ぐためにエラーをスローします。
以下のような場合にこのエラーに遭遇しやすくなります:
react-router-domを使用したルートベースのコード分割の実装時。- TanStack Query (React Query) でsuspense: trueフラグを有効にした時。- 実験的なuse()フックを使用してレンダリング内で直接データを取得した時。## 解決策:Suspense境界の実装この修正は2分で終わります。問題のコンポーネントを<Suspense>タグで囲み、fallbackプロップを指定するだけです。このプロップは、メインコンテンツの読み込み中に何を表示すべきかをReactに明確に指示します。
例1:遅延ロードされるルートの修正もし App.js が以下のスニペットのようになっている場合、ユーザーがダッシュボードに移動した瞬間にクラッシュします:
import React, { lazy } from 'react';
const HeavyDashboard = lazy(() => import('./pages/HeavyDashboard'));
function App() {
return (
<div>
<HeavyDashboard /> {/* ❌ これがエラーを引き起こします */}
</div>
);
}
これを修正するには、Suspenseコンポーネントを導入し、ローディング状態を定義します:
import React, { lazy, Suspense } from 'react';
const HeavyDashboard = lazy(() => import('./pages/HeavyDashboard'));
function App() {
return (
<div>
<Suspense fallback={<div className="spinner">ダッシュボードを読み込み中...</div>}>
<HeavyDashboard />
</Suspense>
</div>
);
}
例2:きめ細かなフォールバック vs グローバルなフォールバックすべてのコンポーネントにラッパーが必要なわけではありません。一つの上位レベルの境界で、複数の遅延ロードされる子コンポーネントをキャッチできます。ただし注意点として、いずれかの子コンポーネントがサスペンドすると、ブロック全体がフォールバックに置き換わります。
<Suspense fallback={<GlobalLoader />}>
<Header />
<LazySidebar />
<LazyMainContent />
</Suspense>
上の例では、サイドバーの読み込み中に Header も消えてしまいます。コンテンツの読み込み中もナビゲーションを表示し続けるには、より戦略的に境界をネストさせます:
<Header />
<div className="layout">
<Suspense fallback={<SidebarSkeleton />}>
<LazySidebar />
</Suspense>
<Suspense fallback={<MainSkeleton />}>
<LazyMainContent />
</Suspense>
</div>
検証:低速ネットワークでのテストローカル環境は高速すぎて、ローディング状態を確認できないことがあります。修正が機能しているか確認するために、Chrome DevToolsを使用して実環境をシミュレートしましょう:
F12を押して「ネットワーク(Network)」タブを開きます。- 「スロットリングなし(No throttling)」と表示されているドロップダウンを探します。- 「高速3G(Slow 3G)」を選択します(これでおよそ400msのレイテンシがシミュレートされます)。- ページを更新します。- コンポーネントが表示される前に、スケルトンスクリーンやスピナーが表示されることを確認します。## 本番環境向けのプロのアドバイス- fallbackプロップは必須です: たとえ何も表示したくない場合でも、fallback={null}を渡す必要があります。- ネットワークエラーの処理: Suspenseは「待機」のみを処理します。ユーザーのインターネット接続が切れ、JSチャンクのダウンロードに失敗した場合、Suspenseだけでは解決できません。404エラーやネットワークタイムアウトをキャッチするために、Suspense境界を ErrorBoundary で囲んでください。- バンドル解析:webpack-bundle-analyzerなどのツールを使用して、React.lazy()によるインポートが実際に別ファイルとして作成されているか確認してください。

