React Labs: 私達のこれまでの取り組み - 2023年3月版

In React Labs posts, we write about projects in active research and development. We've made significant progress on them since our last update, and we'd like to share what we learned.

March 22, 2023 by Joseph Savona, Josh Story, Lauren Tan, Mengdi Chen, Samuel Susla, Sathya Gunasekaran, Sebastian Markbåge, and Andrew Clark


React Labs 記事では、現在活発に研究・開発が行われているプロジェクトについて述べていきます。前回のアップデートから大きな進展がありましたので、我々が学んだことを共有していきます。


React Server Components

React Server Components (RSC) は、React チームによって設計された新しいアプリケーションアーキテクチャです。

RSC に関する研究成果を最初に共有したのは紹介トークRFC でした。簡単にまとめると、JavaScript バンドルに含まれず、事前に実行される新しい種類のコンポーネントである、サーバコンポーネントを導入する、というものです。サーバコンポーネントはビルド中に実行され、ファイルシステムからの読み込みや、静的なコンテンツのフェッチを行えます。また、サーバ上で実行することも可能であるため、API を作成せずにデータレイヤにアクセスすることができます。サーバコンポーネントからブラウザのインタラクティブなクライアントコンポーネントへと、props を使ってデータの受け渡しができます。

RSC は、サーバセントリックなマルチページアプリケーションのシンプルな “リクエスト/レスポンス” モデルと、クライアントセントリックなシングルページアプリケーションのシームレスなインタラクティブ性を組み合わせて、両方の手法のいいところ取りが可能になります。

前回のアップデート以降、React Server Components RFC をマージして提案を承認しました。React Server Module Conventions の提案に関する大きな課題を解決し、"use client" を使うという規約に関してパートナーとのコンセンサスを得ました。これらの文書は、RSC 互換実装がサポートするべき仕様としても機能します。

最大の変更点は、サーバコンポーネントにおいては async / await を主要なデータフェッチ方法として導入することにしたことです。また、Promise の中身を取り出す新しいフック use を導入してクライアントでのデータロードをサポートする予定です。クライアントのみのアプリにおいては、あらゆるコンポーネントで async / await をサポートすることはできませんが、RSC アプリの構造に似た方法でクライアントオンリーのアプリを作成する場合にはそれができるよう、サポートを追加する予定です。

データフェッチがかなり整理されましたので、クライアントからサーバという逆方向へのデータ送信を行って、データベース更新やフォームの実装ができる方法についても検討しています。サーバー/クライアントの境界を越えて Server Action 関数を渡せるようにすることで、クライアントがそれを呼び出し、シームレスな RPC を実現できるようする、という方法を考えています。Server Action により、JavaScript が読み込まれる前に段階的に動作するようになるフォームを提供することも可能です。

React Server Components は Next.js App Router でリリースされました。これは RSC をプリミティブとして完全採用し深く統合を行ったルータのデモとなっていますが、これが RSC 互換ルータやフレームワークを構築するための唯一の方法というわけではありません。RSC 仕様が提供するものと、その実装が提供するものとの間には明確な区切りがあります。React Server Components は、互換のある複数の React フレームワーク間で動作する、コンポーネント仕様として作られています。

一般的には既存のフレームワークを使用することをお勧めしますが、自分自身のカスタムフレームワークの構築が必要な場合は、それも可能です。独自の RSC 互換フレームワークの構築は、主にバンドラとの深い統合が必要であるという理由により、我々が望むほど簡単なものにはなっていません。現行世代のバンドラはクライアントでの使用には適していますが、サーバとクライアントをまたいだ単一のモジュールグラフの分割を行うことを優先的なサポート項目として設計されてはいません。これが、現在 RSC のプリミティブを組み込んでもらうために、バンドラ開発者と直接協力している理由です。

アセットの読み込み

サスペンス (Suspense) は、コンポーネントのデータやコードがまだ読み込み中の場合に画面に表示する内容を指定するものです。これにより、ページが読み込まれる間や、ルータナビゲーションにより追加データやコードが読み込まれる間、ユーザに段階的にコンテンツを表示させることができます。ただし、ユーザの視点からすると、データロードとレンダーが終わっただけでは、新しいコンテンツの準備が完了という話にはなりません。デフォルトではブラウザはスタイルシート、フォント、画像などを独立して読み込みますが、このため UI のジャンプやレイアウトのシフトが繰り返し発生することがあります。

スタイルシート、フォント、画像の読み込みライフサイクルと Suspense を完全に統合し、コンテンツが表示可能かどうかを判断するために React がそれらを考慮できるようにする作業を進めています。あなたが React コンポーネントを書くやり方を一切変えずに、更新がより一貫性のある快適な方法で動作するようになるでしょう。最適化として、コンポーネントからフォントのようなアセットを手動でプリロードする手段も提供する予定です。

現在、これらの機能を実装しており、近いうちに詳細を共有できる予定です。

ドキュメントメタデータ

アプリ内の様々なページや画面には、<title> タグやページの説明、その他の画面固有の <meta> タグなど、様々なメタデータが存在することでしょう。保守の観点からは、これらの情報をそのページや画面に対応する React コンポーネントに近いところで保持するほうがスケーラブルです。しかし、これらのメタデータの HTML タグは、通常アプリの最上位にあるコンポーネントでレンダーされる、ドキュメント <head> 内に配置する必要があります。

現在、人々がこの問題を解決するテクニックが 2 つあります。

1 つ目は、<title><meta> やその他内側に書いたタグをドキュメント <head> 内に移動するための、特別なサードパーティ製コンポーネントをレンダーする、というものです。これは主要なブラウザでは機能しますが、クライアントサイドの JavaScript を実行しない多くのクライアント、例えば Open Graph パーサなどが存在するため、この技術が普遍的に最適というわけではありません。

2 つ目は、ページを 2 つのパーツに分けてサーバでレンダーする、というものです。まず、メインコンテンツをレンダーして、それ用のタグがすべて収集されます。次に、<head> とそれに対応するタグのレンダーを行います。最後に、<head> とメインコンテンツがブラウザに送信されます。このアプローチは機能しますが、<head> が送信できるようになる前にすべてのコンテンツをレンダーする必要があるため、React 18 のストリーミングサーバレンダラを活用できなくなってしまいます。

以上の理由により、我々は <title><meta>、およびメタデータ用 <link> タグをコンポーネントツリーの任意の場所でレンダーするための組み込みサポートを追加しようとしています。これは完全にクライアント側のコード、SSR、および将来的には RSC を含む、すべての環境で同じように機能する予定です。これについては、近日中に詳細を共有します。

React 最適化コンパイラ

前回のアップデート以降、React の最適化コンパイラである React Forget の設計に積極的に取り組んできました。今まではこれを「自動メモ化コンパイラ」であるとお話ししてきており、ある意味においてはそれは正解です。しかし、このコンパイラを構築することで、私たちは React のプログラミングモデルをさらに深く理解できるようになりました。React Forget をよりよく理解する方法は、それをリアクティビティ自動化コンパイラ (automatic reactivity compiler) として捉えることです。

React の中核概念は、開発者が UI を現在の状態に対する関数として定義する、ということです。コンポーネントロジックは、プレーンな JavaScript の値(数値、文字列、配列、オブジェクト)、そして標準的な JavaScript のイディオム(if/else、for など)を使用して記述します。メンタルモデルは、アプリケーションの state が変更されるたびに React が再レンダーを行う、というものです。このシンプルなメンタルモデルや JavaScript のセマンティクスから離れないことが、React プログラミングモデルにおける重要な原則です。

問題は、React が時々過度にリアクティブになる、すなわち再レンダーが過剰になることがあるということです。たとえば、JavaScript では 2 つのオブジェクトや配列が同一である(同じキーと値を持っている)かどうかを比較する安価な方法がないため、レンダーのたびに新しいオブジェクトや配列を作成すると、React が本来必要である以上の作業を行うことがあります。これは、開発者がコンポーネントを明示的にメモ化して変更に対して過剰に反応しないようにする必要があることを意味します。

React Forget の目標は、React アプリがデフォルトでちょうどよい程度のリアクティビティを有することを保証することです。つまり、state の値に対して意味のある変更が行われたときにのみアプリが再レンダーされるようにします。実装の観点から言えば、自動的にメモ化するということですが、リアクティブ性という枠組みで React と Forget を捉えることが理解の上でより良い方法だと考えています。ひとつの考え方としてはこうです:React は現在、オブジェクトの同一性が変更されたときに再レンダーを行います。Forget を使うと、オブジェクトに意味のある値の変更があったときにのみ React が再レンダーを行うようになります。しかし深い比較によるランタイムコストをかけずに、です。

具体的な進捗としては、前回の報告以降、この自動リアクティビティというアプローチに合わせてコンパイラの設計に大幅な見直しを行い、コンパイラを社内で使用して得られたフィードバックを反映させてきました。昨年末以降コンパイラに大幅なリファクタリングを行ったので、現在では Meta の一部の領域で実運用をスタートしています。本番環境での実績が証明され次第、オープンソース化する予定です。

最後に、多くの方がコンパイラの仕組みに興味を持ってくださっています。コンパイラの動作が証明され、オープンソース化されたときに、もっと多くの詳細を共有できることを楽しみにしています。しかし、現時点で共有できることがいくつかがあります。

コンパイラのコアは、Babel からほぼ完全に切り離されています。コアコンパイラ API は(大まかには)古い AST から新しい AST への変換(ソース位置データを保持しながら)を行うものです。内部では、カスタムコード表現や変換パイプラインを使用して、低レベルのセマンティック解析を行います。ただし、コンパイラへの主要な公開インターフェースは、Babel やその他のビルドシステムプラグインを通すものです。テストのしやすさのために現在 Babel プラグインを作っており、これはコンパイラを呼び出して各関数の新バージョンを生成し元の関数と入れ替える作業を行う、薄いラッパとなっています。

過去数ヶ月間でコンパイラをリファクタリングする中で、私たちは条件分岐、ループ、再代入、ミューテーションなどによる複雑性を確実に扱えるよう、コアコンパイルモデルを洗練させることに焦点を当ててきました。ただし JavaScript には、if/else、三項演算子、for、for-in、for-of など、それらの機能を表現する方法がたくさんあります。最初から言語のすべてをサポートしようとすると、コアモデルを検証できる時期が遅れてしまったことでしょう。そのかわり、私たちは言語の小さいながらも代表的なサブセットから始めました。let/constif/elsefor ループ、オブジェクト、配列、プリミティブ、関数呼び出し、その他いくつかの機能です。コアモデルに対する自信を得て、内部の抽象化を洗練させながら、サポートされる言語サブセットの範囲を拡大していきました。また、まだサポートしていない構文について明確化し、サポートされていない入力に対しては診断情報をログに記録しつつコンパイルをスキップするようにしました。Meta のコードベースでコンパイラを試すためのユーティリティが存在し、サポートされていない中で最も一般的な機能を見つけ出し、それらを次に優先するよう決めることができるようになっています。言語全体をサポートするまで、段階的にサポート範囲の拡大を続けていく予定です。

React コンポーネント内のプレーンな JavaScript をリアクティブにするには、コードが実行していることを正確に理解できる、セマンティクスを深く理解したコンパイラが必要です。このアプローチを取ることで、ドメイン固有言語に制限されるのではなく、JavaScript 言語のすべての表現力を使ってプロダクトコードを記述できる、JavaScript 内リアクティビティシステムを作成しています。

オフスクリーンレンダリング

オフスクリーンレンダリングは今後提供予定の React 機能であり、追加のパフォーマンスオーバーヘッドなしにバックグラウンドで画面をレンダーすることができます。これは、DOM 要素だけでなく React コンポーネントでも機能する content-visibility CSS プロパティ の一形態のようなものと考えることができます。研究の過程で、さまざまなユースケースが見つかりました。

  • ルータは、ユーザが画面遷移を行ったときに瞬時に利用できるよう、バックグラウンドで画面を事前レンダーしておくことができます。
  • タブ切り替えコンポーネントは、非表示タブの state を保持できるため、進行済みの作業を失うことなくユーザがタブ間を切り替えることができます。
  • 仮想化リストコンポーネントは、可視範囲の上下に追加の行をプリレンダーできます。
  • モーダルやポップアップを開いたときに、アプリの残りの部分を「バックグラウンド」モードにして、モーダル以外のすべてのイベントや更新が無効になるようにできます。

ほとんどの React 開発者は、React のオフスクリーン API を直接触ることはありません。代わりに、オフスクリーンレンダリングはルータや UI ライブラリに組み込まれるため、それらのライブラリを使う開発者は追加の作業をせずに、自動的にメリットを享受できます。

重要なのは、コンポーネントの書き方を変えることなしに、あらゆる React ツリーをオフスクリーンでもレンダーできなければならない、ということです。コンポーネントをオフスクリーンでレンダーすると、表示されるまで実際にはマウントが起きず、副作用も実行されなくなります。例えば、初めて表示されたときに useEffect を使って分析データをログ出力するコンポーネントがある場合、プリレンダーのせいで分析データの正確性が乱されることはありません。同様に、コンポーネントがオフスクリーンになると、副作用もアンマウントされます。オフスクリーンレンダリングの重要な機能は、コンポーネントの表示/非表示を切り替えても state が失われないことです。

前回のアップデート以降、Meta では、Android および iOS 用の React Native のアプリにおいて内部的にプリレンダリングの実験バージョンをテストしており、パフォーマンスの改善が得られています。また、サスペンス関連でのオフスクリーンレンダリングの動作の改善も行い、オフスクリーン状態のツリー内でサスペンドが起きても、サスペンスのフォールバックが起きないようにしました。残った作業は、ライブラリ開発者に公開される基本要素を最終決定することです。今年中には RFC を公開し、テストおよびフィードバック用の実験用 API を提供する予定です。

トランジショントレーシング

Transition Tracing API により、React のトランジションが遅くなったことを検出し、なぜ遅くなるのかを調査することができます。前回のお知らせ以降、API の初期設計を完了し、RFC を公開しました。基本的な機能も実装されています。現在、プロジェクトは保留中です。RFC に対するフィードバックを歓迎します。React のパフォーマンス測定ツールをより良くするために開発を再開できるようになることを楽しみにしています。これは、Next.js App Router のように React トランジション上に構築されたルータでは、特に役立ちます。


このページでのアップデートに加えて、私たちのチームは最近、コミュニティのポッドキャストやライブストリームにゲスト出演し、取り組みについてより多くのことをお話しし、質問に答える機会がありました。

この投稿のレビューに協力していただいた Andrew ClarkDan AbramovDave McCabeLuna WeiMatt CarrollSean KeeganSebastian SilbermannSeth WebsterSophie Alpert に感謝します。

お読みいただきありがとうございました。次のアップデートでお会いしましょう!