今後の React ではどの範囲を Suspense で囲むかという設計が重要になってくる
はじめに
今後のReact
ではどの範囲をSuspense
で囲むか
という設計が重要になってくる、という話をSuspense
の説明とともにしていきます!
※React18
がリリースされて1年近く経つので今更感あるかもしれませんが、お付き合いください。
Suspense とは
React18
で正式な機能として実装された機能
※React16.6
で実験的機能として追加されていた
Suspense でできること
- データ取得中ローディング状態の宣言的な記述
- コンポーネント単位での
SSR
- コンポーネント単位での
JS
ロード - コンポーネント単位での
Hydration
Suspense は単にローディングを宣言的に記述できるだけの機能ではない
Suspense
はローディングを宣言的に記述できるもの、といった内容を目にすることが多い印象です。
しかし、Suspense
は単にローディングを宣言的に記述できるだけの機能ではありません。
こちらの discussion では、Suspense
について
アプリケーションのユーザーはコンテンツをより早く表示し、はるかに迅速に操作を開始できるようになります。アプリの最も遅い部分が、高速な部分を引きずることはありません。
と言及しています。
※上記記事にSuspenseについてかなり詳しく書いてありますのでぜひ一読を!
1. データ取得中ローディング状態の宣言的な記述
今まで(Suspenseを利用しない)
export const Comments: FC = () => {
const {comments, loading} = useData();
if (loading) return <div>ローディング中</div>
return <div>{comments.map(comment => (<div>{comment}</div>))}</div>
}
Suspenseを使うと
const Comments: FC = () => {
const {comments} = useData();
return <div>{comments.map(comment => (<div>{comment}</div>))}</div>
}
const Index: FC = () => {
return (
<Suspense fallback={<div>ローディング中</div>}>
<Comments />
</Suspense>
)
}
fallback
にローディング時に表示するコンテンツを渡します。
宣言的に記述することができ、Comments
コンポーネントはコメントのリストを表示するという責務だけを持つことができます。
補足
Suspenseコンポーネント
は子コンポーネント内で
Promise
がthrow
されるとサスペンド状態(ローディング状態)をキャッチ。
サスペンド状態のキャッチでfallback
コンテンツ表示する。
おまけ:SWRを利用して Suspense を使う
Suspense
はSWR
などのデータフェッチ系ライブラリと併用して利用していくのが一般的になるかと思います。
SWR
を利用してSuspense
の機能を使うためにはオプションsuspense: true
を設定します。
const Comments: FC = () => {
const { data: comments } = useSWR(key, fetcher, {
suspense: true
});
return <div>{comments.map(comment => (<div>{comment}</div>))}</div>
}
const Index: FC = () => {
return (
<Suspense fallback={<div>ローディング中</div>}>
<Comments />
</Suspense>
)
}
TanStack Query (旧React Query)などもSuspense
に対応しております。
前提:SSR と Hydration について
前提理解として、SSR
とHydration
について軽く触れておきます。
SSR とは
サーバー側でデータフェッチ、HTML
、CSS
の構築などの各処理を実行してからクライアントに描画データを返却すること
SSRの流れ
- クライアントから
Webサーバー
にリクエスト -
Webサーバー
でリクエストを受け取りAPIサーバー
に必要なデータをリクエスト -
APIサーバー
からデータを取得 -
Webサーバー
でAPIサーバー
から取得したデータをもとにHTML
を構築 -
Webサーバー
がHTML
をクライアント(ブラウザ)に返却 -
HTML
(静的な状態)を描画 -
JavaScript
をロードする - ロードした
JavaScript
をHTML
にHydrate
する -
Hydrate
が完了し、Interactive
な状態になる
※Hydration, Hydrate とは
サーバー側で生成されたHTML
にJS
の各ロジックを接続していくこと。
つまり静的な状態のHTML
要素にJS
のイベントハンドラなど接続していき、動的ウェブページに変換していく処理のこと。
クリックなどのイベントが発火されない静的な状態のUIをクリック可能な動的なUIにする処理。
SSR のメリット
- クライアントのマシンやブラウザのスペックに影響しない
- リクエスト毎に常にページが最新の状態になる
- SEOに強い
などなど
SSR のデメリット
- ページ全体のデータフェッチを待たないとページを表示できない
- ページ全体の
JavaScript
ロードを待たないとHydration
を開始できない - ページ全体の
Hydration
を待たないとIntaractive
な状態にならない
などがあると思います。
1. ページ全体のデータフェッチを待たないとページを表示できない
HTML
をブラウザに返す前に重いデータ、軽いデータ関係なく全てのデータをフェッチしなければならず、それによりページ初期表示が遅くなります。
2. ページ全体の JavaScript ロードを待たないと Hydration を開始できない
ページ全体のJavaScript
のサイズが大きい場合、Hydration
開始まで時間がかかってしまいます。
3. ページ全体の Hydration を待たないと Intaractive な状態にならない
Hydration
が完了しているコンポーネントが存在しても、Hydration
が遅いコンポーネントの処理が終了するまで他のコンポーネントもInteractive
な状態にはなりません。
そのため、クリックなどの操作が可能になるまでに他のコンポーネントの処理を待つ必要があります。
これらのデメリットをSuspense
で解消できることをこれから説明していきます。
ポイントとしては
Suspense
で囲ったコンポーネントと、その他の部分とで各処理が可能になることにあります。
2. コンポーネント単位での SSR
今まで(Suspenseを利用しない)
ページ単位でSSRが実行される
ページ内全てのデータ取得やHTML
構築処理の完了を待ってからHTML
要素が返却される。
デメリット
初期表示が遅くなる。
最もデータフェッチ、HTML
構築が遅い要素(コンポーネント)の処理の完了を待たなければならないため。
例
こういったコンポーネント配置のページを例に考えてみます。
function App() {
return (
<div className="App">
<Sidebar />
<LightData />
<HeavyData />
</div>
);
}
-
Sidebar
:データフェッチが不要なコンポーネント -
LightData
:軽いデータのフェッチが必要なコンポーネント -
HeavyData
:重いデータのフェッチが必要なコンポーネント
ページ単位でデータ取得やHTML構築処理を待つ必要があるため、
データ取得が不要なコンポーネント、取得データが軽いコンポーネントも、他のデータが重いコンポーネントが各処理を終えるまで表示されません。
Suspense を使うと
コンポーネント単位(Suspense で囲った単位)でSSRが実行される
コンポーネント単位でデータ取得、HTML構築の完了を待ってから、コンポーネント単位でHTML要素を返却。
(これがStreaming HTML
というもの)
これにより1.ページ全体のデータフェッチを待たないとページを表示できないというデメリットが解消されます。
データフェッチ、HTML構築が遅い要素(コンポーネント)の処理の完了を待たずページを表示できるため、初期表示が早くなる。
データフェッチ、HTML
構築が遅い要素(コンポーネント)は処理が完了する前にfallback
コンテンツが表示される。
例
コンポーネントをSuspense
で囲います。
function App() {
return (
<div className="App">
<Suspense fallback={<div>ローディング...</div>}>
<Sidebar />
</Suspense>
<Suspense fallback={<div>ローディング...</div>}>
<LightData />
</Suspense>
<Suspense fallback={<div>ローディング...</div>}>
<HeavyData />
</Suspense>
</div>
);
}
※ 青がローディング終了後、表示されている状態 / 白がローディング状態
①最初にデータ取得の必要がないSidebarが表示されます
②次に軽いデータ取得が必要なLightDataが表示されます
③最後に重いデータ取得が必要なHeavyDataが表示されます
3. コンポーネント単位での JS ロード
今まで(Suspenseを利用しない)
ページ全体のJavaScript
ロードを待たないとHydration
を開始できない。
JavaScript
のサイズが大きいコンポーネントがある場合、サイズの小さいコンポーネントもHydration
開始まで時間がかかってしまう。
Suspense を使うと
コンポーネント単位(Suspense で囲った単位)で JavaScript ロードが実行される
ページ全体のJavaScript
ロードを待つ必要がなく、コンポーネント単位でHydration
を開始することができます。
これにより2. ページ全体のJavaScript
ロードを待たないと Hydration
を開始できないというデメリットが解消されます。
4. コンポーネント単位での Hydration
今まで(Suspenseを利用しない)
ページ単位でHydration
処理が完了する。
ページにある全てのコンポーネントのHydration
処理を待たないと、どのコンポーネントもIntaractive
な状態になりません。
Suspense を使うと
コンポーネント単位(Suspense
で囲った単位)でHydration
処理が行なわれます。
これにより、3. ページ全体のHydration
を待たないと Intaractive
な状態にならないというデメリットが解消されます。
Suspense を用いいることでHydration処理がコンポーネント単位でできるようになった
// React.lazy()と組み合わせて利用する必要があるようです
import { lazy } from 'react';
const Sidebar = lazy(() => import('./Sidebar.js'));
const LightData = lazy(() => import('./LightData.js'));
const HeavyData = lazy(() => import('./HeavyData.js'));
function App() {
return (
<div className="App">
<Suspense fallback={<div>ローディング...</div>}>
<Sidebar />
</Suspense>
<Suspense fallback={<div>ローディング...</div>}>
<LightData />
</Suspense>
<Suspense fallback={<div>ローディング...</div>}>
<HeavyData />
</Suspense>
</div>
);
}
仮に3つのコンポーネントでHTML
が静的な状態で表示されていて、それらのコンポーネントはHydration
処理はまだ行われていない状態とします。
その場合、React
はコンポーネントツリーの早い段階で見つかった(
つまり上から順番に見つかった)Suspense
で囲ったコンポーネントからHydration
処理を開始します。
今回の場合はSidebarコンポーネント
から開始されます。
※ 緑が動的な状態 / 黄がHydration中状態 / 白がHydrationを開始していないかつ静的な状態
① SidebarでHydration開始
② LightDataでHydration開始
③ HeavyDataでHydration開始
④ 全てのコンポーネントがIntaractiveな状態に
更にユーザーの関心対象を検知して優先的にHydrationしてくれる機能がある
デフォルトでは先述したように、コンポーネントツリーの早い段階で見つかったもの(配置されている要素の上)から順にHydration
処理が行われます。
しかし、ユーザーの関心対象のコンポーネントを検知してHydration
を行ってくれる機能があります。
これがSelective Hydration
という機能。
このSelective Hydration
という機能がすごい。
どんな動きをするかというと
① SidebarでHydration開始
② ユーザーがHeavyDataをクリックする
③ SidebarのHydrationを中断し、HeavyDataでHydration開始
React
がユーザーの関心対象を検知して、HeavyDataコンポーネント
を優先的にHydration
しようとする。
④ HeavyDataが先にIntaractiveな状態に
こんな動作をしてくれるようです。
さいごに
最初にも申し上げたとおり
今後のReact
ではどの範囲をSuspenseで囲むか
という設計が特にパフォーマンスの観点から重要になってくると思います。
ただ、まだ実際のプロダクトでSuspense
を導入できている訳ではないので今後実際の開発で試していくなかで知見をためていきたいです。
(どこまでの粒度、単位でSuspense
を利用していくべきなのかなど)
最後までお読みいただきありがとうございました!
参考資料
ありがとうございました!
Discussion