React SuspenseとErrorBoundaryを組み合わせたウォーターフォール通信の回避

React Frontend Performance TypeScript
結論

非独立データは Promise.all 等で並列起動し、直列的な Suspense 停止を防ぎます。

// React公式仕様:Promise.all による通信並列化と Suspense の組み合わせ
import { Suspense, use } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

// 1. 通信を並列起動(Promise.all)
async function fetchDashboardData() {
  const [user, posts] = await Promise.all([
    fetch('/api/user').then(res => res.json()),
    fetch('/api/posts').then(res => res.json()),
  ]);
  return { user, posts };
}

// 2. コンポーネント側で受ける
function DashboardContent({ dataPromise }) {
  const { user, posts } = use(dataPromise); // React 19 use フック
  return <div>{user.name} の投稿数: {posts.length}</div>;
}

React公式仕様:なぜウォーターフォール問題が起きるのか?

React公式ドキュメント(react.dev/reference/react/Suspense)で警告されている通り、親コンポーネントと子コンポーネントがそれぞれ別個に非同期データ取得を行う設計になっている場合、以下の遅延(Render-then-Fetch)が発生します:

  1. 親コンポーネントがマウントされ、親の通信が開始
  2. 親の通信が終わるまで Suspense の fallback が表示
  3. 親の通信完了後に初めて子コンポーネントが描画され、子の通信が開始
  4. 再度 Suspense が発生し、合計待機時間が「親の通信時間 + 子の通信時間」に増大

実際に起こる障害:階段状ネットワーク遅延事故

DevTools の Network タブで通信ログを確認した際、リクエストが階段状(ウォーターフォール)に伸びている場合、本来並列で取れるはずのAPI群が直列実行され、ユーザーの待ち時間が2〜3倍に跳ね上がる表示遅延障害 が発生しています。


解決手順

  1. 互いに依存しないデータ群(ユーザー情報・投稿一覧等)は、個別のコンポーネント内で独立して fetch を呼ぶのをやめる
  2. 親コンポーネントまたはルートレベルで Promise.all([fetchA(), fetchB()]) を呼び出し、同時にリクエストを発行する
  3. SuspenseErrorBoundary でコンポーネント全体を包み、ローディングとエラー表示を一括・型安全に管理する
// ⭕ 並列通信化と ErrorBoundary / Suspense の構成パターン
export default function DashboardPage() {
  const dataPromise = fetchDashboardData(); // マウント前に並列リクエスト着手

  return (
    <ErrorBoundary fallback={<div>通信エラーが発生しました</div>}>
      <Suspense fallback={<div>ダッシュボードを読み込み中...</div>}>
        <DashboardContent dataPromise={dataPromise} />
      </Suspense>
    </ErrorBoundary>
  );
}