React.lazy と Suspense によるコード分割

必要以上のコードをユーザーに提供する必要はありません。このような事態を避けるために、バンドルを分割してください。

React.lazy メソッドを使用すると、動的インポートを使用して React アプリをコンポーネント レベルで簡単にコード分割できます。

import React, { lazy } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

const DetailsComponent = () => (
  <div>
    <AvatarComponent />
  </div>
)

メリット

通常、大規模な React アプリケーションは、多くのコンポーネント、ユーティリティ メソッド、サードパーティ ライブラリで構成されます。必要なときだけアプリケーションのさまざまな部分を読み込もうとしない場合、ユーザーが最初のページを読み込んだら、すぐに単一の大きな JavaScript バンドルがユーザーに提供されます。これはページのパフォーマンスに大きな影響を与える可能性があります。

React.lazy 関数を使用すると、わずかな手間で、アプリケーションのコンポーネントを JavaScript のチャンクに分割できます。その後、Suspense コンポーネントと結合することで、読み込み状態を処理できます。

サスペンス

サイズの大きい JavaScript ペイロードをユーザーに配信する際の問題は、特に脆弱なデバイスやネットワーク接続では、ページの読み込みが完了するまでに時間がかかることです。このため、コード分割と遅延読み込みは非常に有用です。

ただし、コード分割コンポーネントがネットワーク経由でフェッチされる際は、常にわずかな遅延が発生するため、有用な読み込み状態を表示することが重要です。Suspense コンポーネントで React.lazy を使用すると、この問題を解決できます。

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

const renderLoader = () => <p>Loading</p>;

const DetailsComponent = () => (
  <Suspense fallback={renderLoader()}>
    <AvatarComponent />
  </Suspense>
)

Suspensefallback コンポーネントを受け入れ、任意の React コンポーネントを読み込み状態として表示できます。次の例は、その仕組みを示しています。アバターはボタンがクリックされた場合にのみレンダリングされ、一時停止された AvatarComponent に必要なコードを取得するリクエストが発行されます。その間に、フォールバック読み込みコンポーネントが表示されます。

ここでは、AvatarComponent を構成するコードが小さいため、読み込みスピナーが短時間しか表示されません。大きなコンポーネントの場合、特にネットワーク接続が不安定な場合、読み込みに時間がかかることがあります。

この仕組みをわかりやすく説明するには、次のようにします。

  • サイトをプレビューするには、[アプリを表示] を押してから、全画面表示 全画面表示 を押します。
  • Ctrl+Shift+J キー(Mac の場合は Command+Option+J キー)を押して DevTools を開きます。
  • [Network] タブをクリックします。
  • [スロットリング] プルダウンをクリックします。デフォルトでは [スロットリングなし] に設定されています。[Fast 3G] を選択します。
  • アプリの [Click Me] ボタンをクリックします。

読み込みインジケーターの表示時間が長くなります。AvatarComponent を構成するすべてのコードが、個別のチャンクとして取得されています。

1 つの chunk.js ファイルがダウンロード中であることを示す DevTools のネットワーク パネル

複数のコンポーネントの一時停止

Suspense のもう一つの特長は、複数のコンポーネントがすべて遅延読み込みされる場合でも、その読み込みを一時停止できることです。

次に例を示します。

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));
const InfoComponent = lazy(() => import('./InfoComponent'));
const MoreInfoComponent = lazy(() => import('./MoreInfoComponent'));

const renderLoader = () => <p>Loading</p>;

const DetailsComponent = () => (
  <Suspense fallback={renderLoader()}>
    <AvatarComponent />
    <InfoComponent />
    <MoreInfoComponent />
  </Suspense>
)

これは、単一の読み込み状態のみを表示しながら、複数のコンポーネントのレンダリングを遅らせる非常に便利な方法です。すべてのコンポーネントの取得が完了すると、ユーザーはすべてのコンポーネントが同時に表示されます。

これは、次の埋め込みで確認できます。

これがないと、「段階的読み込み」や、UI の異なる部分が 1 つずつ順番に読み込まれるという個別の読み込みインジケーターの問題に遭遇しやすくなります。その結果、ユーザーの利便性が損なわれる可能性があります。

読み込みエラーを処理する

Suspense を使用すると、ネットワーク リクエストが内部で行われている間、一時的な読み込み状態を表示できます。しかし、これらのネットワーク リクエストがなんらかの理由で失敗した場合はどうなるでしょうか。オフラインになっているか、ウェブアプリがサーバーの再デプロイ後に古くて利用できなくなったバージョニングされた URL を遅延読み込みしようとしている可能性があります。

React には、このような読み込みの失敗を適切に処理するための標準パターンとして、エラー境界を使用します。ドキュメントに記載されているように、React コンポーネントはライフサイクル メソッド static getDerivedStateFromError() または componentDidCatch() のいずれか(または両方)を実装することで、エラー境界として機能します。

遅延読み込みの失敗を検出して処理するには、エラー境界として機能する親コンポーネントで Suspense コンポーネントをラップします。エラー境界の render() メソッド内で、エラーがなければ子をそのままレンダリングし、問題が発生した場合はカスタムエラー メッセージをレンダリングできます。

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));
const InfoComponent = lazy(() => import('./InfoComponent'));
const MoreInfoComponent = lazy(() => import('./MoreInfoComponent'));

const renderLoader = () => <p>Loading</p>;

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {hasError: false};
  }

  static getDerivedStateFromError(error) {
    return {hasError: true};
  }

  render() {
    if (this.state.hasError) {
      return <p>Loading failed! Please reload.</p>;
    }

    return this.props.children;
  }
}

const DetailsComponent = () => (
  <ErrorBoundary>
    <Suspense fallback={renderLoader()}>
      <AvatarComponent />
      <InfoComponent />
      <MoreInfoComponent />
    </Suspense>
  </ErrorBoundary>
)

まとめ

React アプリケーションにコード分割を適用する場所がわからない場合は、次の手順を行います。

  1. ルートレベルから開始します。ルートは、アプリケーションの分割可能なポイントを識別する最も簡単な方法です。Suspensereact-router とともに使用する方法については、React のドキュメントをご覧ください。
  2. ページ上の特定のユーザー操作(ボタンのクリックなど)でのみレンダリングされる大きなコンポーネントを特定します。これらのコンポーネントを分割すると、JavaScript ペイロードを最小限に抑えることができます。
  3. 画面外にあり、ユーザーにとって重要でないものは、すべて分割することを検討してください。