React Labs: これまでの取り組み – 2023年3月

2023年3月22日 Joseph Savona, Josh Story, Lauren Tan, Mengdi Chen, Samuel Susla, Sathya Gunasekaran, Sebastian Markbåge, Andrew Clark


React Labs の記事では、現在研究開発中のプロジェクトについて執筆しています。前回のアップデートから、大きな進歩がありましたので、そこで得られた学びを共有したいと思います。


React Server Components

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

RSC に関する調査は、まず紹介講演RFCで共有しました。それらを要約すると、新しい種類のコンポーネントである Server Components を導入します。これは事前に実行され、JavaScript バンドルから除外されます。Server Components はビルド中に実行できるため、ファイルシステムから読み取ったり、静的コンテンツをフェッチしたりできます。また、サーバー上で実行して、API を構築せずにデータレイヤーにアクセスすることもできます。Server Components からブラウザ内のインタラクティブな Client Components に props を介してデータを渡すことができます。

RSC は、サーバー中心のマルチページアプリのシンプルな「リクエスト/レスポンス」のメンタルモデルと、クライアント中心のシングルページアプリのシームレスなインタラクティブ性を組み合わせて、両方の長所を提供します。

前回のアップデート以降、提案を批准するためにReact Server Components RFCをマージしました。React Server Module Conventionsの提案に関する未解決の問題を解決し、パートナーとの間で"use client"の規約を採用することに合意しました。これらのドキュメントは、RSC に準拠した実装がサポートすべき内容の仕様としても機能します。

最大の変更点は、Server Components からデータをフェッチする主な方法として、async / await を導入したことです。また、Promise をアンラップする新しい Hook である use という名前の Hook を導入することで、クライアントからのデータローディングもサポートする予定です。クライアントのみのアプリの任意のコンポーネントでasync / awaitをサポートすることはできませんが、クライアントのみのアプリを RSC アプリと同様の構造で構成する場合には、そのサポートを追加する予定です。

データフェッチはかなりうまく整理できたので、次は別の方向、つまりクライアントからサーバーにデータを送信して、データベースの変更を実行したり、フォームを実装したりする方法を検討しています。これを行うために、サーバー/クライアント境界を越えて Server Action 関数を渡すことで、クライアントが呼び出すことができ、シームレスな RPC を提供できるようにします。また、Server Actions では、JavaScript がロードされる前にプログレッシブに強化されたフォームを提供します。

React Server Components はNext.js App Routerでリリースされました。これは、RSC をプリミティブとして捉えたルーターの深い統合を示していますが、RSC 互換のルーターとフレームワークを構築する唯一の方法ではありません。RSC 仕様と実装によって提供される機能には明確な分離があります。React Server Components は、互換性のある React フレームワーク間で動作するコンポーネントの仕様として意図されています。

一般的には既存のフレームワークを使用することをお勧めしますが、独自のカスタムフレームワークを構築する必要がある場合も可能です。RSC 互換のフレームワークの構築は、必要な深いバンドラー統合のため、私たちが望むほど簡単ではありません。現在の世代のバンドラーはクライアントでの使用には最適ですが、サーバーとクライアントの間で単一のモジュールグラフを分割するためのファーストクラスのサポートは考慮されていませんでした。これが、RSC のプリミティブを組み込むために、現在バンドラーの開発者と直接提携している理由です。

アセットのロード

Suspense を使用すると、コンポーネントのデータやコードがまだ読み込まれている間、画面に表示するものを指定できます。これにより、ユーザーはページの読み込み中や、より多くのデータとコードを読み込むルーターナビゲーション中に、より多くのコンテンツを段階的に表示できます。ただし、ユーザーの視点から見ると、データロードとレンダリングだけでは、新しいコンテンツの準備が整っているかどうかを判断する際に、すべてを語ることはできません。デフォルトでは、ブラウザはスタイルシート、フォント、および画像を個別に読み込むため、UI のジャンプや連続的なレイアウトシフトが発生する可能性があります。

React がコンテンツを表示する準備ができたかどうかを判断する際に、スタイルシート、フォント、および画像のローディングライフサイクルを考慮するように、Suspense と完全に統合することに取り組んでいます。React コンポーネントの作成方法を変更することなく、アップデートはより一貫性があり、快適な動作になります。最適化として、コンポーネントからフォントなどのアセットを直接プリロードする手動の方法も提供します。

現在これらの機能を実装しており、近日中にさらに共有する予定です。

ドキュメントメタデータ

アプリ内の異なるページや画面は、<title>タグ、説明、およびこの画面に固有の他の<meta>タグなど、異なるメタデータを持つ場合があります。保守の観点からすると、この情報をそのページまたは画面のReactコンポーネントの近くに保持する方がよりスケーラブルです。ただし、このメタデータのHTMLタグは、通常アプリの最上位のコンポーネントでレンダリングされるドキュメントの<head>内にある必要があります。

今日、人々はこの問題を次の2つの手法のいずれかで解決しています。

1つの手法は、<title><meta>、およびその他のタグをドキュメントの<head>に移動する特別なサードパーティコンポーネントをレンダリングすることです。これは主要なブラウザでは機能しますが、Open Graphパーサーなど、クライアントサイドのJavaScriptを実行しないクライアントも多数存在するため、この手法はすべての場合に適しているわけではありません。

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

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

React最適化コンパイラ

前回のアップデート以来、Reactの最適化コンパイラであるReact Forgetの設計を積極的に繰り返してきました。以前は「自動メモ化コンパイラ」として話していましたが、ある意味ではその通りです。しかし、コンパイラを構築することで、Reactのプログラミングモデルをさらに深く理解することができました。React Forgetを理解するより良い方法は、自動リアクティビティコンパイラとして捉えることです。

Reactのコアアイデアは、開発者が現在の状態の関数としてUIを定義することです。数値、文字列、配列、オブジェクトなどのプレーンなJavaScript値を使用し、if/else、forなどの標準的なJavaScriptイディオムを使用してコンポーネントロジックを記述します。メンタルモデルは、アプリケーションの状態が変化するたびにReactが再レンダリングするというものです。このシンプルなメンタルモデルと、JavaScriptのセマンティクスに密接に保つことが、Reactのプログラミングモデルにおける重要な原則であると考えています。

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

React Forgetでの私たちの目標は、Reactアプリがデフォルトで適切な量のリアクティビティを持つようにすることです。つまり、状態の値が意味的に変化した場合にのみ、アプリが再レンダリングされるようにすることです。実装の観点からすると、これは自動的にメモ化することを意味しますが、リアクティビティのフレームワークの方がReactとForgetを理解するのに適していると考えています。このことを考える1つの方法は、Reactは現在、オブジェクトの同一性が変化したときに再レンダリングするということです。Forgetを使用すると、セマンティックな値が変化したときにReactが再レンダリングされますが、ディープ比較のランタイムコストは発生しません。

具体的な進捗状況としては、前回のアップデート以降、この自動リアクティビティのアプローチに沿い、社内でのコンパイラの使用からのフィードバックを取り入れるために、コンパイラの設計を大幅に繰り返してきました。昨年末からコンパイラに対していくつかの大きなリファクタリングを行った後、現在、Metaの一部の領域で本番環境でコンパイラを使用し始めています。本番環境でそれを証明したら、オープンソース化する予定です。

最後に、多くの人がコンパイラの仕組みに興味を示しています。コンパイラを証明してオープンソース化したら、より多くの詳細を共有することを楽しみにしています。しかし、今共有できることがいくつかあります。

コンパイラのコアはBabelからほぼ完全に分離されており、コアコンパイラAPIは(おおよそ)古いASTを入力し、新しいASTを出力します(ソースロケーションデータを保持しながら)。内部では、低レベルのセマンティック分析を行うために、カスタムのコード表現と変換パイプラインを使用しています。ただし、コンパイラの主要なパブリックインターフェースは、Babelやその他のビルドシステムプラグイン経由になります。テストを容易にするために、現在、コンパイラを呼び出して各関数の新しいバージョンを生成し、それをスワップインする非常に薄いラッパーであるBabelプラグインがあります。

過去数か月間コンパイラをリファクタリングするにあたり、条件分岐、ループ、再代入、ミューテーションなどの複雑さを処理できるように、コアコンパイルモデルの改良に焦点を当てたいと考えました。ただし、JavaScriptには、if/else、三項演算子、for、for-in、for-ofなど、これらの各機能を表現する方法がたくさんあります。言語全体を事前にサポートしようとすると、コアモデルを検証できるポイントが遅れてしまいます。代わりに、言語の小さくても代表的なサブセットから始めました。let/const、if/else、forループ、オブジェクト、配列、プリミティブ、関数呼び出し、およびその他のいくつかの機能です。コアモデルに自信を持ち、内部抽象化を改良するにつれて、サポートされる言語サブセットを拡張しました。また、まだサポートしていない構文については明確にしており、診断をログに記録し、サポートされていない入力についてはコンパイルをスキップしています。Metaのコードベースでコンパイラを試してみて、最も一般的なサポートされていない機能を確認し、次の優先順位を付けるためのユーティリティを用意しています。引き続き、言語全体をサポートするように段階的に拡張していきます。

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

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

オフスクリーンレンダリングは、パフォーマンスのオーバーヘッドを追加することなく、バックグラウンドで画面をレンダリングするためのReactの今後の機能です。これは、DOM要素だけでなくReactコンポーネントにも機能するcontent-visibility CSSプロパティのバージョンと考えることができます。私たちの調査中に、さまざまなユースケースを発見しました。

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

ほとんどのReact開発者は、ReactのオフスクリーンAPIを直接操作することはないでしょう。代わりに、オフスクリーンレンダリングはルーターやUIライブラリなどに統合され、それらのライブラリを使用する開発者は追加の作業なしで自動的にメリットを享受できます。

この考え方は、コンポーネントの書き方を変えることなく、任意のReactツリーをオフスクリーンでレンダリングできるようにするべきだということです。コンポーネントがオフスクリーンでレンダリングされる場合、コンポーネントが可視になるまで実際にはマウントされません。つまり、そのエフェクトは発火しません。たとえば、コンポーネントが最初に表示されたときに分析ログを記録するためにuseEffectを使用する場合、プリレンダリングによってこれらの分析の精度が損なわれることはありません。同様に、コンポーネントがオフスクリーンになると、そのエフェクトもアンマウントされます。オフスクリーンレンダリングの重要な機能は、状態を失うことなくコンポーネントの表示/非表示を切り替えることができることです。

前回のアップデート以降、私たちはMeta社内でAndroidとiOSのReact Nativeアプリでプリレンダリングの実験バージョンをテストし、良好なパフォーマンス結果を得ています。また、オフスクリーンレンダリングがSuspenseと連携する方法も改善しました。オフスクリーンツリー内でサスペンドしても、Suspenseのフォールバックはトリガーされません。残りの作業は、ライブラリ開発者に公開するプリミティブを最終決定することです。年内にRFCを公開し、テストとフィードバックのための実験的なAPIを公開する予定です。

トランジション追跡

トランジション追跡APIを使用すると、Reactトランジションが遅くなったときを検出し、遅くなる可能性のある理由を調査できます。前回のアップデートに続き、APIの初期設計が完了し、RFCを公開しました。基本的な機能も実装済みです。このプロジェクトは現在保留中です。RFCへのフィードバックをお待ちしており、Reactのパフォーマンス測定ツールをより良いものにするために開発を再開することを楽しみにしています。これは、Next.js App Routerのように、Reactトランジションの上に構築されたルーターで特に役立ちます。


このアップデートに加えて、私たちのチームは最近、コミュニティのポッドキャストやライブストリームにゲスト出演し、私たちの仕事について詳しく説明し、質問に答えています。

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

お読みいただきありがとうございます。次回の更新でお会いしましょう!