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

必要以上のコードをユーザーに配布する必要はないため、バンドルを分割して、このようなことが起こらないようにしてください。

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

import React, { lazy } from 'react';

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

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

なぜこれが有用なのでしょうか。

大規模な React アプリケーションは通常、多くのコンポーネント、ユーティリティ メソッド、サードパーティ ライブラリで構成されます。アプリのさまざまな部分を必要に応じてのみ読み込むように工夫していない場合、ユーザーが最初のページを読み込むとすぐに、JavaScript の 1 つの大きなバンドルがユーザーに送信されます。これにより、ページのパフォーマンスが大幅に低下する可能性があります。

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 を開きます。
  • [ネットワーク] タブをクリックします。
  • [スロットリング] プルダウンをクリックします。デフォルトでは [スロットリングなし] に設定されています。[高速 3G] を選択します。
  • アプリの [Click Me] ボタンをクリックします。

読み込みインジケーターが表示される時間が長くなりました。AvatarComponent を構成するすべてのコードが個別のチャンクとしてフェッチされることに注目してください。

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

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

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

次に例を示します。

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>
)

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

次の埋め込みコードで確認できます。

これを行わないと、読み込みのずれが発生しやすくなります。UI のさまざまな部分が次々と読み込まれ、それぞれに読み込みインジケーターが表示されます。これにより、ユーザー エクスペリエンスがより不快なものになる可能性があります。

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

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

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

遅延読み込みの失敗を検出して処理するには、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. ルートレベルから始めます。ルートを使用すると、分割可能なアプリケーションの点を簡単に特定できます。React のドキュメントでは、Suspensereact-router とともに使用する方法について説明しています。
  2. 特定のユーザー操作(ボタンのクリックなど)でのみレンダリングされる、サイトのページ上の大きなコンポーネントを特定します。これらのコンポーネントを分割すると、JavaScript ペイロードを最小限に抑えることができます。
  3. 画面外にあり、ユーザーにとって重要でないその他のコンテンツは分割することを検討してください。