React 18 Suspense 遅延読み込みとパフォーマンスを理解する 基礎
遅延読み込み
遅延読み込みは、最適化手法やデザインパターンの一つです。
それは画像、ビデオ、Webページ、音楽ファイル、ドキュメントなど、必要になるまで読み込みを遅らせて貴重なデータを節約する手法です。
通常、Reactのシングルページアプリケーション(SPA)は小さいため、問題なく動作します。しかし、コンテンツ管理システムなどの複雑なアプリケーションを扱う際には、プログラム全体を一度に読み込むことは理想的ではありません。
そこで、Reactアプリケーションを本番用にする前に、Webpackなどのプリインストールされたバンドラーを使用してプロジェクトをパッケージ化します。
しかし、このパッケージ化されたプロジェクトを読み込むと、ユーザーが滅多にアクセスしないページも含めて、ソースコード全体が一度に読み込まれてしまいます。
遅延読み込みはこの問題を解決するために開発されました。
アプリの重要でない部分の読み込みを遅らせることで、DOMの読み込み時間を短縮し、アプリケーションのパフォーマンスを向上させることができます。このようにして、必要な時にのみコードを読み込むため、初期ページ読み込み時間が短縮される利点があります。
Reactでは、遅延読み込みをシンプルかつ簡単に実装するための2つのネイティブ機能があります。
一つはReact.lazy()であり、もう一つはReact.Suspenseです。
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"));
動的インポートと通常のインポートの違い
では、動的インポートと通常のインポートの違いの違いです。
これら2つのインポートの主な違いは、動的インポートでは、式に到達したときにコンポーネントをインポートします。
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 OtherComponent from './OtherComponent.js'; const App = () => { return <OtherComponent /> }
通常のインポートでは、コンポーネントを静的にインポートしているので、使用されるかどうかに関係なく、すべてのコンポーネントがユーザーに配布するコードにバンドルされます。
つまり、通常のインポートはアプリケーション全体に使用されるコンポーネントに適しています。
React Suspenseとは?
ReactのSuspense機能は、React v16.6で導入された比較的新しい機能ですが、その後もReactチームは機能の改良と追加を進めてきました。
React 18では、Suspenseに新機能が追加され、より強力で柔軟な遅延ローディングのサポートが得られるようになりました。
Suspenseは、ファーストパーティのReactコンポーネントで、非同期リクエストを行う可能性のある他のコンポーネントをラップするために使用されます。子コンポーネントがネットワークリクエストなどの読み込み状態になるアクションを実行する際、<Suspense>
コンポーネントはレンダリングを切り替えてSpinnerのような読み込み中のUIを表示できます。
これは外部APIからデータを読み込んでいる場合、ユーザーに読み込み中であることを通知するために非常に便利です。
また、アニメーションを読み込む際にもSuspenseが使用されます。
Suspenseは、コンポーネントが現在読み取っているデータの準備ができていないことを通知し、Reactがデータの準備が完了するのを待ってからUIを更新します。コンポーネントがキャッシュからデータをロードしている間、レンダリングを一時停止するため、外部ソースからデータをフェッチしているアプリの優れたユーザーエクスペリエンスを確保するために一般的に使用される方法です。
React 18では、Suspenseに以下のような新機能が追加されました
・ データが解決されるまで、新しい状態遷移の作成を保留するようにコンポーネントに通知します。
・ ネストされたプレースホルダーとそれに続くプレースホルダーコンポーネントの表示を遅くすることで、UIスラッシュを軽減します。
・ 複数のコンポーネントをユーザーに公開する必要がある順序で配置することにより、複数のコンポーネントの一時停止を支援します。
React 18の自動バッチ処理により、変更の影響は最小限に抑えられます。
ただし、アプリのReactバージョン18への移行には注意が必要です。アプリのコードに依存する具体的な変更や注意点が存在する可能性があるため、公式ドキュメントや移行ガイドを参照してスムーズな移行をサポートすることが重要です。
まず、あなたがReactのSuspenseについて学習する前に覚えておかなければいけない事があります。
それは、エラー境界となります。
ReactのSuspense機能は、別名で「Suspense Boundaries(サスペンス境界)」とも呼ばれます。
これはReactのエラー境界(Error Boundaries)という概念に由来しています。
エラー境界
エラー境界は、エラーをスローする可能性のある他のコンポーネントをラップするために使用されるコンポーネントです。
子コンポーネントがネットワーク要求の失敗などのエラーをスローするたびに、ラップしているエラー境界コンポーネントがそのレンダリングを切り替えて、カスタムエラーUIを表示できます。
<ErrorBoundary fallback={<ErrorMessage />}> <MyErrorProneComponent /> </ErrorBoundary>
サスペンス境界では子コンポーネントの読み込みに反応し、エラー境界はエラーになった子コンポーネントに反応します。
これら2つは同じJavaScriptの命令ステートメントであるthrowを使って動作します。
JavaScriptでは、エラーだけでなく何でも投げることができます。
つまり、エラー境界とSuspenseが一緒に働くことで、読み込み中、エラー発生中、あるいは完全に正常な状態など、どんな状態でも対応できる安定したユーザーインターフェイスを作ることが可能となります。
それらの概念を視覚化しコードに変換すると、2つの概念がどのように絡み合っているかが明確になります。
エラー境界は、関数コンポーネントではreact-error-boundarという、Error Boundaryを関数コンポーネントでも使用できるラッパーを提供しています。
react-error-boundaryパッケージを使用していきます。
エラー境界の作成および、詳しく学ばれたい方は下記のReact Docsリファレンスガイドおよび当記事のチュートリアルで解説しておりますので参照下さい。
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ステートメントを使用する必要があります。
例えば、以下のようにします。
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 OtherComponent1 = () => { if (/* エラー条件を満たす場合 */) { throw new Error("エラーメッセージ1"); } return ( <div> <h1>OtherComponent1</h1> </div> ); }; const OtherComponent2 = () => { // 他のコンポーネントも同様にエラー条件を追加することができます return ( <div> <h1>OtherComponent2</h1> </div> ); }; // (中略) const App = () => ( <div> <ErrorBoundary> <Suspense fallback={<div>Loading...</div>}> <LazyComponent1 /> <LazyComponent2 /> <LazyComponent3 /> <LazyComponent4 /> </Suspense> </ErrorBoundary> </div> );
OtherComponent1のような非同期でロードされるコンポーネント内でエラー条件を設定し、それが満たされる場合にthrowステートメントを使用してエラーを発生させることで、ErrorBoundaryコンポーネントがエラーをキャッチし、fallbackプロパティで指定されたローディング中のUIを表示することができます。
これにより、アプリケーション全体がクラッシュするのを防ぎ、ユーザーにエラーメッセージを適切に表示することができます。
コンポーネントがロードされていない間は、フックは実際には使用できないことがわかります。
コンポーネントは、Promiseをスローせずにレンダリングが完了する前に、レンダリングの試行ごとにアンマウント/マウントされます。
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のライブラリを活用することで、アプリにルートベースのコード分割を容易に設定できます。
最新のReact v18では、Suspenseコンポーネントが強化され、より一般的に使用できるようになりました。
これにより、コンポーネントの一時停止をフレキシブルに制御できるようになります。
たとえば、コンポーネントがネットワークからデータをフェッチしている間は、フォールバックロードコンポーネントを表示させることができます。データのフェッチが完了して表示の準備ができると、コンポーネントは再開されます。
Suspenseコンポーネントが一時停止すると、その親ツリー内で最も近いコンポーネントがそれをキャッチすることができます。この親コンポーネントのことをSuspense境界と呼びます。Suspenseがエラーをスローした場合でも、最も近い親コンポーネントがそのエラーをキャッチします。
さらに、SSR(サーバーサイドレンダリング)でのSuspenseでは、Suspense境界でコンポーネントをラップすることで、サーバーからコンポーネントのストリーミングを行えます。
これにより、時間のかかるコンポーネントを待たずに、サーバーでレンダリングされたコンポーネントのHTMLをクライアントにストリーミングできます。
これらの機能を活用して、アプリのバンドルを均等に分割し、ユーザーエクスペリエンスを妨げることなく、効果的なコード分割を実現することが可能です。
以下が、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の最新機能を駆使して、よりスマートなアプリケーションを構築しましょう。Suspenseの活用は、コード分割を簡素化し、ユーザーエクスペリエンスを向上させる鍵となります。
これにより、アプリのパフォーマンスが向上し、ユーザーにより快適な体験を提供できることでしょう。
React 18のSuspenseは以前に以下でも触れておりますので合わせて一読下さい。
注意点
注意すべき重要な点は、Suspenseに渡されるfallbackプロパティとなります。
ネットワーク呼び出しが終了するのを待っている間にレンダリングしたいものです。
fallbackには、読み込み中のUIを表示するためにスピナーやスケルトンローダー、または何も表示しない場合には適切なコンテンツを指定できます。
Reactはネットワーク・リクエストの終了を待っている間、fallbackの値が何であれレンダリングします。その後、非同期コンポーネントがロードされると、そのコンポーネントがレンダリングされます。
このfallbackプロパティは、ユーザーエクスペリエンスを向上させるために非常に重要であり、アプリケーションが非同期データを取得している間にユーザーにローディング中であることをわかりやすく伝えることができます。
最後に
Suspenseの概念を理解するのは、単純な事ではないかもしれません。
しかし、技術的にはReact 18を使用する場合、SuspenseはReactツリーのどこでも使用できます。
これは、クライアント側レンダリング(CSR)、サーバー側レンダリング(SSR)、およびReact Serverコンポーネント(RSC)のいずれにも含まれます。
ただし、React 17以前のプロジェクトの場合、Suspenseはクライアント側レンダリングでのみ使用できることに注意してください。
遅延読み込みは、ページのパフォーマンスを向上させながらユーザーをサイトにとどめるための優れた方法です。適切に使用すれば、効率的で使いやすいソリューションを構築するのに役立ちます。
当ブログでの解説を通じて、Suspenseの内部の仕組みが少しでも明確になったことを願っています。
SuspenseはReactアプリケーションにおいて非常に強力で重要な機能であり、遅延読み込みやエラーハンドリングなどの面で役立つことができます。
熟練して使いこなすことで、よりスムーズなユーザーエクスペリエンスを提供することができるでしょう。
本日は以上となります。
最後まで読んで頂きありがとうございます。
この記事が役に立ったら、ブックマークと共有をしていただけると幸いです。