React 19 RCのSuspenseに関する問題と現状のまとめ

2024/06/17に公開

この記事は、React 19 RCにおけるSuspenseの問題について、なるべくフラットな立場から現状を解説することを目的としたものです。

この問題の経緯については既に先行の記事が存在します。本記事は、この記事とは別の切り口でまとめ、現在の動向を追加したものとなるよう構成しました。
https://zenn.dev/mylifeasjosh/articles/d12e231adbde15

要約

React公式が正式リリースまでに対応してくれるようなので、大人しく待ちましょう。

概要

以下のように<Suspense>で包まれた、2つの兄弟コンポーネント<Foo><Bar>があるとします。

<Suspense fallback={...}>
  <Foo />
  <Bar />
</Suspense>

プリレンダリングを走らせる際、React 18では<Foo><Bar>は並列にレンダリングされましたが、React 19 RCでは直列にレンダリングされるという変更が適用されました。

なぜ変更を適用したか

直列にレンダリングすることで得られるメリットがあります。それは、レンダリングの途中経過をスタックに保存できるということです。

途中経過をスタックに保存できれば、Suspenseの位置まで遡ることも、Suspense内部のレンダリングの途中経過を再利用することも容易となります。

また、PRを参照すると、このタイミングでレンダリングを直列に変更した理由として以下の2つが挙げられています。

  • Loading状態の描画
  • use APIへの対応

これらは、Fiber(Reactの仮想DOM)が兄弟要素のレンダリングの並列化をサポートすることでも解決できるようですが、この実装にはかなりの時間がかかるようです。

Loading状態の描画

Suspenseはpropsにfallbackを指定することで、Loading状態の描画をすることができます。スケルトンと呼ばれるUIがこれに該当します。React 18ではプリレンダリングの際、並列化された非同期処理の完了を待ってからfallbackが描画されるため、Loading状態の描画にブロックがかかってしまっていました。これを解消するためのシンプルな方法として、直列化が採用されました。レンダリングが直列化されれば、スタックを辿ってLoading状態をすぐさま描画することが容易になります。

use APIへの対応

React 19 RCでは新しい機能としてuse APIが追加されました。これにより、Clientコンポーネントのレンダリングの途中でPromiseが解決を待つということが起きるようになりました。Promiseが解決した後、途中までのレンダリング結果を再利用するため、スタックに結果を蓄積するモチベーションが高くなったようです。

何が問題になっているか

この変更はServer Componentsを利用してdata fetchingしていることを前提として出されたものですが、現実にはSPAの形式をとっているプロジェクトが多数あります。SPAのプロジェクトにとってこの変更は大きな影響を持ちます。

Fetch Waterfallによるパフォーマンスの低下

PRのコメントに実測値を上げている方がいらっしゃいました。このコメントによると、React 18からReact 19 RCにバージョンアップしたところ、今まで並列に実行されていたfetchが直列に実行され、パフォーマンスが大きく低下したとのことです。
React 18を利用した時はfetchが並列に実行される
React 18を利用した時のfetchの様子
React 19 RCを利用したfetchが直列に実行される
React 19 RCを利用した時のfetchの様子

「関心の分離」との矛盾

Reactにおける基本的な設計思想の一つとして関心の分離(Separation of Concerns)というものがあります。関心の分離とは、技術的な都合でソースコードを分離するのではなく、「何に関心があるか、何を実現したいか」という軸でソースコードを分離することを指します。Reactではコンポーネントという形でまとめることで、関心の分離を実現しています。React 19 RCで追加されたmetadataへの対応stylesheetへの対応もこの一環であると考えられます。

しかしながら、上記Fetch Waterfallによるパフォーマンスの低下を避けようとすると、データを実際に利用するコンポーネントから祖先のコンポーネントへdata fetchingに関するロジックを移動させる必要が出てきます。これはReactが推進している関心の分離とは矛盾するため、問題視されています。

data fetching以外の用途で生じる問題

Suspenceの主な用途はdata fetchingで用いるものですが、それ以外にも一般に非同期タスクであればなんでもSuspenceを利用することができます。例えば、バイナリデータのデコードや3Dの描画などが挙げられます。こういった用途で利用されたSuspenseもレンダリングの直列化による影響を大きく受けることになります。

提案された解決策

Reactチームはこの問題に関して、(現段階では口約束のみですが)対応する方針を示しています。
https://x.com/sophiebits/status/1801663976973209620

React 19の正式リリース時にはなにかしらの対応がなされると思われます。

GitHubでは新たなissueが作られ、議論が続けられています。そこでは、strategy={"parallel" | "sequential"}のように、レンダリングを直列にするか並列にするか選べるオプションを提供することを検討しています。直列、並列のいずれにもメリット・デメリットがあるため、ユーザーが選択できるようにしたいという意図があるようです。この書き方が採用されれば、data fetchingに関してServer Componentsを用いる場合はsequentialを、SPAで使う場合はparallelを指定することになるでしょう。

まとめ

React界隈でちょっとした騒ぎになったこの問題ですが、GitHubに集まった建設的な意見とReactチームの柔軟な対応により、いい感じの着地点に落ち着きそうな雰囲気です。

参考リンク

GitHubにおいて、議論の対象となったのは以下のPRです。
https://github.com/facebook/react/pull/26380

上記PRはすでにMergeされたものであるため、現在は以下のissueで議論が続けられています。
https://github.com/facebook/react/issues/29898

Discussion