deve.K

エンジニアが未来を切り開く。

React 18 Suspense 遅延読み込みとパフォーマンスを理解する 基礎

React Suspense

遅延読み込み

遅延読み込みは最適化手法またはデザインパターンです。

画像、ビデオ、Webページ、音楽ファイル、またはドキュメントのいずれであっても、必要になるまで読み込みを遅らせ、貴重なデータを節約します。

デフォルトのReactはコードベース全体をバンドルし、同時にデプロイします。

Reactのシングルページアプリケーション(SPA)は非常に小さいため、通常は問題ございません。

ただし、コンテンツ管理システムなどのより複雑なアプリを使用している場合、プログラム全体をすぐに読み込むことは理想的ではありません。

Reactアプリケーションをプロダクション対応にする前に、 Webpackなどのプリインストールされたバンドラーを使用してパックされます。

このパックされたプロジェクトが読み込まれると、ユーザーがめったにアクセスしないページも含めて、ソースコード全体が一度に読み込まれてしまいます。

遅延読み込みは、その動作を阻止するために開発されました。

プロセスをごまかして、アプリの重要でない部分の読み込みを延期し、必要に応じて読み込まれるようにして、DOMの読み込み時間を短縮し、アプリケーションのパフォーマンスを向上させます。

仮にすべてがダウンロードされていなくても、ユーザーはWebサイトにアクセスが可能性となります。

遅延読み込みの利点は、初期ページ読み込み時間が短縮されます。

遅延ロードされた画像は、データと帯域幅を節約するので、高速インターネットや大規模なデータプランを持っていない個人にとって特に便利です。

コンポーネントの一部のみを要求することで、サーバーとクライアントのリソースを節約します、つまりリソースの保持です。

Reactには、遅延読み込みの実装を非常にシンプルかつ簡単にする2つのネイティブ機能がございます。

React.lazy()

React.Suspense

React.lazy()

React lazyは、Reactの比較的新しい関数であり、追加のサードパーティライブラリの助けを借りずにコード分割を通じて、Reactコンポーネントを遅延して読み込むことができます。

この関数を使用すると、動的インポートを通常のコンポーネントとしてレンダリングできます。

const OtherComponent = React.lazy(() => import('./OtherComponent'));

const LazyComponent1 = lazy(() => new Promise(resolve => {
  setTimeout(() => {
    resolve({ default: () => <OtherComponent1 /> });
  }, 3000);
}));

動的にロードされ、通常のコンポーネントとしてレンダリングされるコンポーネントを簡単に作成できます。

コンポーネントレンダリングされると、それを含むバンドルが自動的にロードされます。

React.lazy()でコンポーネントをロードした後に Promiseを提供する必要があるメソッドを引数として受け入れます。

解決されたPromiseは、Reactコンポーネントを含むデフォルトのエクスポートを持つモジュールを指しています。

React lazy()は現在、デフォルトのエクスポートのみをサポートしています。

インポートしたいモジュールが名前付きエクスポートを使用する場合、これらの名前付きエクスポートをデフォルトのエクスポートとして再エクスポートする中間モジュールを作成して、React lazy()で使用できるようにすることができます。

export const MyComponent = () => {}

名前付きエクスポートをデフォルト エクスポートとして再エクスポートするにはさまざまな方法があります。

export { MyComponent as default } from "./ManyComponents.js";

// App.js
import React, { lazy } from 'react';

const MyComponent = lazy(() => import("./MyComponent.js"));

動的インポートと通常のインポートの違い

では、動的import()と通常のimport()の違いです。

これら2つのインポートの主な違いは、動的import()では、式に到達したときにコンポーネントをインポートします。

let OtherComponent = undefined;

if (false) {
 OtherComponent = React.lazy(() => import('./OtherComponent.js'));
}

この関数は、動的にインポートされたコンポーネントを返すことが期待されるコールバックを受け入れます。

Reactの動的インポートはコンパイル時にモジュールをバンドルしません。

ページの読み込み時に小さな初期バンドルを出荷できるため、帯域幅がいくらか節約されます。

コンポーネントとインポートされたその他のアセット (CSSや画像など)も延期されます。

問題が1つございます。

動的にインポートされたコンポーネントのフォールバックUIを指定する必要があります。

<Suspense fallback={<div>Loading...</div>}>
      <OtherComponent />
 </Suspense>

上記では、Reactが特別に作ったSuspenseコンポーネントで、fallback Propsを受け取り、遅延コンポーネントをラップしています。

これは後に詳しく解説します。

一方で、通常のimport()では、コンポーネント呼び出しでコンポーネントをインポートします。

import OtherComponent from './OtherComponent.js';
 
const App = () => {

  return <OtherComponent />
}

通常のインポートでは、コンポーネントを静的にインポートしているので、使用されるかどうかに関係なく、すべてのコンポーネントがユーザーに配布するコードにバンドルされます。

React Suspenseとは?

ReactのSuspense機能は、しばらく前から存在しています。

2018年10月にサスペンスは、React v16.6で導入された比較的に新しいReact機能となっております。

数年前から利用可能になっているにもかかわらず、私たちのほとんどはアプリでSuspenseを使用していません。

ではいつ使用するのか?

実用的には、SuspenseはファーストパーティのReactコンポーネントで、非同期リクエストを行う可能性のある他のコンポーネントをラップするために使用されます。

コンポーネントがネットワークリクエストなどの読み込み状態になるアクションを実行するたびに、ラップされた< Suspense >< /Suspense >コンポーネントレンダリングを切り替えてSpinnerのような読み込み中のUIを表示することができます。

Loading SpinnerはAPI fetchでローディング画面を表示する必要がある親コンポーネントと統合する方法で、データが外部 APIから読み込まれていることをユーザーに通知します。

アニメーションを読み込む際にも使用されます。

Suspenseはコンポーネントが現在読み取っているデータの準備ができていない事を通知するためになります。

読み取ったデータをReactが準備が完了するのを待ちます、その後に『UI』を更新していきます。

コンポーネントがキャッシュからデータをロードしている間レンダリングを一時停止しますので、外部ソースからデータをフェッチしているアプリの優れた『UX』を確保するために使用される、ごく一般的な方法となっております。

コンポーネントのツリーを『バックグラウンド』でレンダリングが可能となります。

Suspenseはreact-asyncのようなデータ取得ライブラリではなく、 Reduxのような状態を管理する方法でもないことに注意することが重要です。

React 18では、同時レンダリング実行機能に基づく完全なサスペンス機能と、次のものが含まれます。

データが解決されるまで、新しい状態遷移の作成を保留するようにコンポーネントに通知します。

ネストされたプレースホルダーとそれに続くプレースホルダコンポーネントの表示を遅くすることで、UI スラッシュを軽減します。

複数のコンポーネントをユーザーに公開する必要がある順序で配置することにより、複数のコンポーネントの一時停止を支援します。

React 18は、以前のバージョンとは異なる方法でサスペンスを処理します。

ただし、自動バッチ処理により変更の影響は最小限に抑えられます。

アプリのReact バージョン18への移行には実質的な影響はありません。

まず、あなたがReactのSuspenseについて学習する前に覚えておかなければいけない事があります。

それは、『エラー境界』となります。

Suspenseには、別の呼び方(別名)がございます。

『Suspense Boundaries』という名前でも使用できます、これはReactの同様の概念『エラー境界』に由来しています。

エラー境界

エラー境界は、エラーをスローする可能性のある他のコンポーネントをラップするために使用されるコンポーネントです。

コンポーネントがネットワーク要求の失敗などのエラーをスローするたびに、ラップしているエラー境界コンポーネントがそのレンダリングを切り替えて、カスタムエラーUIを表示できます。

<ErrorBoundary fallback={<ErrorMessage />}>
 <MyErrorProneComponent />
</ErrorBoundary>

サスペンス境界では子コンポーネントの読み込みに反応し、エラー境界はエラーになった子コンポーネントに反応します。

これら2つは同じJavaScriptの命令ステートメントであるthrowを使って動作します。

JavaScriptでは、エラーだけでなく何でも投げることができます。

つまり、エラー境界とSuspenseが一緒に働くことで、読み込み中、エラー発生中、あるいは完全に正常な状態など、どんな状態でも対応できる安定したユーザーインターフェイスを作ることが可能となります。

それらの概念を視覚化しコードに変換すると、2つの概念がどのように絡み合っているかが明確になります。

エラー境界は、関数コンポーネントではreact-error-boundarという、Error Boundaryを関数コンポーネントでも使用できるラッパーを提供しています。

react-error-boundaryパッケージを使用していきます。

エラー境界の作成および、詳しく学ばれたい方は下記のReact Docsリファレンスガイドおよび当記事のチュートリアルで解説しておりますので参照下さい。

Error Boundary – React – JP

Error Boundary – React – US

dev-k.hatenablog.com

Suspenseの使い方

コンポーネントで保留中のネットワークリクエストを処理するSuspenseの最も単純な例を見てみましょう。

const [users, isLoading] = fetchData('/users')

if (isLoading) {
  return <Spinner />
}

return <Users data={users} />

上記は、一般的なネットワーク呼び出しの待機を処理する方法です。

isLoadingという変数で、リクエストの状態を追跡しています。

trueの場合であれば、Spinnerをレンダリングし、ユーザーに状態を伝えます。

この方法は、特に問題はございません。

これを、Suspenseとしてどのように処理するのか?

const users = fetchData('/users')

return (
  <Suspense fallback={<Spinner />}>
    <Users data={users} />
  </Suspense>
);

上記は、重要な変更があります。

ロード状態を状態変数として持ち、その値に基づいてSpinnerをレンダリングするロジックではなく、ReactがSuspenseを使用し管理するようになりました。

フォールバックを宣言的にレンダリングするようになったという事です。

fallback Propsは、元のコンテンツが読み込まれる前にコンポーネントを表示することができます。

Suspenseでラップしない場合での、isLoading変数は読み込み状態を管理する必要があります。

Suspenseを使用する事によって、Reactはネットワーク呼び出しが発生していることを認識し、UsersコンポーネントをSuspenseでラップすることで、ネットワーク呼び出しが完了するまでレンダリングを遅延させることが可能性となります。

Reactに重いコンポーネントレンダリングを遅らせるように指示し、まずは最も軽いコンポーネントが最初にロードしキャッチされます。

それでは、SuspenseとError Boundariesを使用する個々のコンポーネントの例を見てみましょう。

const App = () => {
  return (
<ErrorBoundary fallback={<ErrorMessage />}>
  <Suspense fallback="Loading...">

<AsynchronousComponent />

  </Suspense>
</ErrorBoundary>
  );
};

最上位であるAppコンポーネントは3つのコンポーネントレンダリングします。

< ErrorBoundary >

< Suspense >

< AsynchronousComponent >

ErrorBoundaryではその下にあるエラーをキャッチし、それに応じてUIを切り替えるコンポーネントです。

Suspenseでは下にあるものから、読み込み状態を検出します、それに応じてUIを切り替えるコンポーネントです。

AsynchronousComponentでは、何かしらの非同期処理を行うコンポーネントとなります、APIにネットワーク・リクエストを行うなどです。

また、複数の遅延コンポーネントをサスペンスコンポーネント内に配置可能です。

下記のアプローチは、すべてのコードをブラウザで実行するクライアント側レンダリング(CSR)のWebプロジェクトを対象としています。

import { Suspense, lazy } from "react";
import {ErrorBoundary} from 'react-error-boundary'

const LazyComponent1 = lazy(() => import("./OtherComponent1"));
const LazyComponent2 = lazy(() => import("./OtherComponent2"));
const LazyComponent3 = lazy(() => import("./OtherComponent3"));
const LazyComponent4 = lazy(() => import("./OtherComponent4"));

const LazyComponent1 = lazy(() => new Promise(resolve => {
  setTimeout(() => {
    resolve({ default: () => <OtherComponent1 /> });
  }, 4000);
}));

const LazyComponent2 = lazy(() => new Promise(resolve => {
  setTimeout(() => {
    resolve({ default: () => <OtherComponent2 /> });
  }, 4000);
}));

const LazyComponent3 = lazy(() => new Promise(resolve => {
  setTimeout(() => {
    resolve({ default: () => <OtherComponent3 /> });
  }, 4000);
}));

const LazyComponent4 = lazy(() => new Promise(resolve => {
  setTimeout(() => {
    resolve({ default: () => <OtherComponent4 /> });
  }, 4000);
}));

const OtherComponent1 = () => {
return (
<div>
<h1>OtherComponent1</h1>
</div>
 )
}

const OtherComponent2 = () => {
return (
<div>
<h1>OtherComponent2</h1>
</div>
 )
}

const OtherComponent3 = () => {
return (
<div>
<h1>OtherComponent3</h1>
</div>
 )
}

const OtherComponent4 = () => {
return (
<div>
<h1>OtherComponent4</h1>
</div>
 )
}

const App = () => (
    <div>
        <ErrorBoundary>
            <Suspense fallback={<div>Loading...</div>}>
                <LazyComponent1 />
                <LazyComponent2 />
                <LazyComponent3 />
                <LazyComponent4 />
            </Suspense>
        </ErrorBoundary>
    </div>
);

React.lazy()を使用した場合、import()関数はPromiseを返します、このプロミスは、ネットワーク障害、ファイルが見つからないエラー、ファイルパスエラーなどにより拒否される可能性があります。

遅延コンポーネントの周りにエラー境界を配置する必要があります。

解決されたPromiseは4秒後に表示されます。

See the Pen React Suspense & lazy & reactErrorBoundary by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.


上記の通り、Loading...が4秒間表示されます。

コードの実行を停止するには、throwステートメントを使用する必要があります。

コンポーネントがロードされていない間は、フックは実際には使用できないことがわかります。

コンポーネントは、Promiseをスローせずにレンダリングが完了する前に、レンダリングの試行ごとにアンマウント/マウントされます。

REST API リクエストでご確認下さい。

import { Suspense, lazy } from "react";
import {ErrorBoundary} from 'react-error-boundary';

import { useState, useEffect } from "react";

const LazyComponent2 = lazy(() => new Promise(resolve => {
  setTimeout(() => {
    resolve({ default: () => <OtherComponent2 /> });
  }, 4000);
}));

const OtherComponent1 = () => {
return (
<div>
<h1>OtherComponent1</h1>
</div>
 )
}

const OtherComponent2 = () => {
const [users, setUsers] = useState([])
useEffect(() => {
const url = "https://jsonplaceholder.typicode.com/users";

const fetchUsersData = async () => {
try {
const res = await fetch(url);
const json = await res.json();
setUsers(json)
console.log(json);
} catch (e) {
console.log("error", e);
 }
}

fetchUsersData(); //呼び出し
}, [])
return (
<div>
<h1>OtherComponent2</h1>
  <ul>
{users.map((user) => <li key={user.id}> {user.name} </li>)}
</ul>
</div>
 )
}

 )
}

const OtherComponent4 = () => {
return (
<div>
<h1>OtherComponent4</h1>
</div>
 )
}

const App = () => (
    <div>
          <OtherComponent1 />
          <OtherComponent3 /> 
          <OtherComponent4 />

        <reactErrorBoundary>
            <Suspense fallback={<div>Loading...</div>}>

               <LazyComponent2 />
              
            </Suspense>
        </reactErrorBoundary>
    </div>
);

See the Pen React Suspense & lazy & reactErrorBoundary APIリクエスト by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.


では、React RouterとReact lazy()のライブラリを使用して、アプリにルートベースのコード分割を設定する方法を下記に示します。

import  { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
 
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
 
const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </Router>
);

アプリでコード分割を導入する場所を決定するのは、少し難しい場合があります。

バンドルを均等に分割するが、ユーザーエクスペリエンスを妨げない場所を選択する必要があります。

ですが、上記のようにReact lazyとSuspenseでコード分割を設定可能です。

React v18では、Suspenseコンポーネントの機能を強化し、より一般化できるようになりました。

コンポーネントは、ネットワークからデータをフェッチしている間など、中断できるタイミングを自由に選択できるようになっております。

そのため、コンポーネントがデータをフェッチしている場合、コンポーネントは一時停止しコンポーネントがネットワークからフェッチしたデータを表示する準備ができるまで、フォールバックロードコンポーネントを表示できます。

コンポーネントが一時停止すると、その親ツリーで最も近いコンポーネントがそれをキャッチします。

その場合、Suspense境界という用語になります。

コンポーネントのSuspenseがthrowエラーのリンクであると考えられる場合、コード内の深さに関係なく、最新のブロックがそれをキャッチします。

Suspense境界についてはまた別途記事に致します。

SSRでのSuspenseでは、サスペンス境界でラップして、サーバーから両方をストリーミングできるようになります。

つまり、時間がかかる可能性のあるコンポーネントを待たずに、サーバーでレンダリングされたコンポーネントのクライアントへの準備が整ったHTMLのストリーミングを開始できます。

React 18のSuspenseは以前に下記でも触れておりますので合わせて一読下さい。

dev-k.hatenablog.com

注意点

注意すべき重要な点は、Suspenseに渡されるfallbackプロパティとなります。

ネットワーク呼び出しが終了するのを待っている間にレンダリングしたいものです。

spinnerまたはskeleton loaderでも、あるいは何もしないでもかまいません。

Reactはネットワーク・リクエストの終了を待っている間、fallbackの値が何であれレンダリングします。

最後に

Suspenseの概念を理解するのは、単純な事ではないかもしれません。

技術的には、少なくとも先述したようにReact 18を使用している場合でのSuspenseはReactツリーのどこでも使用可能です。

これは、クライアント側レンダリング(CSR)、サーバー側レンダリング(SSR)およびReact Serverコンポーネント(RSC)が含まれています。

React 17以前でのプロジェクトの場合のSuspenseはクライアント側レンダリングでのみ使用が制限されておりますのでご注意下さい。

遅延読み込みは、ユーザーをサイトにとどめながらページのパフォーマンスを向上させる優れた方法となります。

適切に使用すれば、効率的で使いやすいソリューションを構築するのに役立ちます。

あなたが、Suspenseの内部の仕組みが少しでも明確になったことを願っております。

本日は以上となります。

最後まで読んで致しありがとうございます。

プライバシーポリシー