Ichizokuは日本唯一のSentry公認販売業者です。 日本語のドキュメント、動画、サポート窓口で日本のお客様のSentry活用を支援します。

Reactの「フェッチウォーターフォール」について

早速ですが、以下のような問題に遭遇したことはありますか?

または、これはどうですか?

これはおそらく見たことがあるでしょう。

ちなみに、上記で挙げたものはすべて同じです。
1つ目の画像はSentryのイベント詳細ページ、2つ目はChromeのネットワークタブ、そして3つ目は、それを引き起こす原因となるコードスニペットです。

もし上の質問に「はい」と答えた方は、今回の記事が大いに役立つと思います。
そうでない場合でも、将来の自分のためにぜひ読んでみてください。

これは「フェッチウォーターフォール」と呼ばれ、Reactにおける一般的なデータフェッチ問題です。

これはデータをフェッチし、「ローディング」状態を表示してから子コンポーネントをレンダリングする(そして同様のことをするなど)コンポーネントの階層を作成するときに発生します。

ざっくりいうと、Webページを表示するために必要なデータをサーバから取得する際に起こる問題であるということです。

各コンポーネントのデータが親のものに依存する場合には問題ありませんが、常にそうとは限りません。
各フェッチが少なくとも1秒かかる場合、ページのロードに3秒以上かかることになります。現代のデジタルネイティブにとって、3秒のロード時間はとても長く感じますよね。
しかし、並行してフェッチすれば1秒で得られるデータを、3秒以上も待たせる理由は一体何なのでしょうか?
この問題のCodeSandboxがこちらですので、ご自身の目でご確認ください。

良いパフォーマンスを維持するため、3つの方法をご紹介します。
そして、各ソリューションを使用したい場面を探っていきましょう。

対処法① Suspenseを使用する

Suspenseは、フェッチウォーターフォールを避けるための有効な手段の1つです。これは、コンポーネントツリー全体のフェッチを並行してトリガーするため、データがずっと速くフェッチされます。しかしこの方法は本番環境に適していません。

Reactのドキュメントでは、Suspense対応データソースのみがSuspenseコンポーネントを起動し、それらはRelayNext.jsといったフレームワーク、またはlazyでの遅延ロードコンポーネント、あるいはuseでの約束値の読み取りであると説明しています。

遅延ロードとuseフックも必ずしも良い対応策だとはいえません。
遅延ロードコンポーネントは、フェッチウォーターフォールを確実に排除することはありません。それは同じように振る舞い、Suspenseからの「ローディング」フォールバックが唯一の利点です。
useフックはReactのcanaryバージョンでのみ利用可能なため、まだ安定版としてリリースされていません。

ですので、Next.jsやRelayを使用していない限り、Suspenseを対処法として使うことはお勧めしません。

しかし、本当に使いたい場合は、このCodeSandboxの例を確認してください。本番環境に適したものが出れば、良いパフォーマンスを維持し、フェッチウォーターフォールを避ける素晴らしい方法になり得ます。

対処法② サーバー上でデータをフェッチする

Next.jsを使用する場合は、サーバーコンポーネントを使用します。
そうすれば、クライアントはデータとともにHTMLを受信し、データ要求をする必要がなくなります。
リクエストがない……つまりフェッチウォーターフォールがない!ということになります。

では、このようにサーバーサイドでデータをフェッチするのでしょうか?

サーバーは、クライアントに処理結果を返す前に、一連のデータをすべてフェッチするのを待つことになります。
これは並列実行の問題であって、レンダリングの問題ではありません。
サーバーでデータをフェッチするだけでは解決することができません。

上の画像を見ると、確かに滝のように見えますね。

古典的なSSR(または “loading “コンポーネントのないServer Components)では、TTFBはデータをフェッチする時間よりも遅くなっています。
そして、TTFBの値が大きくなったので、他のすべてのウェブバイタルも大きくなっています。

Next.jsでもローディングコンポーネントを提供することでTTFBを修正できますが、それでもウォーターフォールは落ち続けます。

もうひとつ考慮すべきことは、SSR/Server Componentsを使用する場合でも、ブラウザ上でレンダリングするということです。
クライアントは、サーバーから受け取ったDOMを表示し、ユーザーのインタラクションに反応できるようにしなければなりません。
つまり、サーバーで生成した静的なWebページに、クライアント側のスクリプトが操作可能にする必要があります。

  1. 2台のコンピュータ(ブラウザとサーバー)がレンダリングタスクを実行します。
  2. サーバーは各リクエストに対してより多くのタスクを実行します。
  3. ブラウザは(SSRのとき)ユーザーにはまだ空白のページを表示しており、レスポンスが返ってきたときにもまだレンダリングを行っています。

サーバーでのフェッチは悪いアイデアではありません。
しかしその方法を使う場合は「トレードオフ」であることを踏まえて検討する必要があります。もし問題がないのであれば、このように並列にデータをフェッチするようにしてください。

3つのfetchメソッドは、他のAPIリクエストやデータベース呼び出しなど、どんなものでも構いませんが、Promiseを返さなければなりません。Promise.allはすべての入力プロミスを並行して起動し、入力プロミスの最後が解決されたときに解決される新しいプロミスを返します。

フェッチ・ウォーターフォールを避けるためにデータ・フェッチを持ち上げる

フェッチ・ウォーターフォールの問題を解決するもう1つの方法は、データ・フェッチをコンポーネント階層の上位レベルに引き上げることです。
コンポーネント・レベルでデータをフェッチする代わりに、コンポーネント・ツリーの最上位レベル(データをフェッチし始める最初のコンポーネント)でデータをフェッチし、それをコンポーネント・ツリーに渡すことができます。

この場合でも、並行してデータをフェッチすることは重要なので、必ずそのようにしてください。

つまり、最初のコンポーネントがマウントされたときにデータをフェッチし、それからデータを下に渡すということです。

React Contextでどのようにhoisted data fetchingを実装できるか、CodeSandboxのサンプルを用意しました。以下の画像をご覧ください。

コンテキスト・プロバイダを使えば、子コンポーネントはすべて、プロップ・ドリリングなしで確認したいデータにアクセスできます。

pikachuリクエストと/pikachu/encountersリクエストは同時に開始され、/location-areaリクエストは/pikachu/encountersリクエストの結果に依存しますが、これらも同時に開始されます。これでウォーターフォールを解除することができました。

フェッチウォーターフォールをなくすには、データフェッチの処理を巻き取るのが最も普遍的なアプローチでしょう。
クライアント・サイドで行われるため、サーバーの負担が減り、アプリのウェブバイタルも改善されます。
Susppenseのような実験的な機能に依存することもなく、Next.jsやRelayを使用する場合にのみ機能するわけでもありません。

React Contextで取得するか、親コンポーネントで取得してpropsで渡すかは、あなた次第です。
コンポーネントの構造がどうなっているか、あなたやあなたのチームが何に慣れているか、プロジェクトで他にどんなライブラリを使用しているかによって異なります。

例えば、TanStack Queryを使用する場合、すべてのフェッチを実行し、結果をコンポーネント階層の上位にキャッシュします。新しいContextを作成したくない場合は、この方法でも代用できます。単に、Context Providerを置く場所にフェッチを作成し、コンポーネント内のプロバイダの代わりにキャッシュからデータを読み取ります。

結論

つまり、フェッチ・ウォーターフォールを引き起こすためにできることは2つあるということです。

  1. 条件付きでレンダリングしながら、自身のデータをフェッチするコンポーネントをネストする。
  2. リクエストを順番に呼び出す(最初にawait、次にawait second、そしてawait thirdといった具合に)。

クライアント側でデータをフェッチしている場合、各コンポーネントでデータをフェッチしながら条件付きでレンダリングしていないか確認してください。
各コンポーネントがそれぞれデータをフェッチし、それらを順次表示すると、データのフェッチが順次トリガーされ、ウォーターフォールになります。

データ取得の仕組みをより上位のコンポーネントに引き上げるのは良い修正です。
構造にもよりますが、親コンポーネントでフェッチしてpropsを通して下に渡すか、データを下に渡すことでpropのドリリングが発生する場合はReact Contextを作成するか、TanStack Queryのようなライブラリをすでに使用している場合は、上位レベルでフェッチを実行し、キャッシュから結果を読み込むためにQueryClientを使用します。

データ・フェッチをサーバーサイドに移したからといって、問題が解決するわけではありません。
各フェッチ・リクエストを個別に待機させても、フェッチ・ウォーターフォールは発生しますが、今度はサーバー側で発生します。
この方法では、クライアントがピクセルの描画を開始する前に、サーバーがすべてのデータ・フェッチを行うのを待つ必要があるため、TTFBウェブ・バイタルも影響を受けます。つまり、あなたは問題を修正しなかっただけでなく、新たな問題を導入したのです。

アプリ開発手法に気を配り、本番環境でのパフォーマンスに目を光らせることが、この競争に勝つための唯一の手段です。


 

IchizokuはSentryと提携し、日本でSentry製品の導入支援、テクニカルサポート、ベストプラクティスの共有を行なっています。Ichizokuが提供するSentryの日本語サイトについてはこちらをご覧ください。またご導入についての相談はこちらのフォームからお気軽にお問い合わせください。

シェアする

Recent Posts