React Labs: 私たちが取り組んでいること – 2023年3月
2023年3月22日 by 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は、サーバー中心のMulti-Page Appsのシンプルな「リクエスト/レスポンス」メンタルモデルと、クライアント中心のSingle-Page Appsのシームレスなインタラクティビティを組み合わせ、両方の世界の最良の部分を提供します。
前回の更新以来、提案を批准するためにReact Server Components RFCをマージしました。React Server Module Conventions提案の未解決の問題を解決し、パートナーと合意して「use client」規約を採用することにしました。これらの文書は、RSC互換実装がサポートすべき仕様としても機能します。
最大の変更は、Server Componentsからのデータ取得の主要な方法としてasync/awaitを導入したことです。また、Promiseをアンラップする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のプリミティブを組み込むためにバンドラー開発者と直接パートナーシップを結んでいる理由です。
Asset Loading
Suspenseを使用すると、コンポーネントのデータやコードがまだ読み込まれている間に画面に表示する内容を指定できます。これにより、ユーザーはページの読み込み中やより多くのデータとコードを読み込むルーターナビゲーション中に、段階的により多くのコンテンツを見ることができます。
しかし、ユーザーの観点から見ると、新しいコンテンツが準備できているかどうかを考慮する際、データの読み込みとレンダリングだけでは全体像を語っていません。デフォルトでは、ブラウザはスタイルシート、フォント、画像を独立して読み込むため、UIのジャンプや連続的なレイアウトシフトが発生する可能性があります。
私たちは、Suspenseをスタイルシート、フォント、画像の読み込みライフサイクルと完全に統合して、Reactがコンテンツを表示する準備ができているかどうかを判断する際にこれらを考慮するように取り組んでいます。Reactコンポーネントの作成方法を変更することなく、更新はより一貫性があり、快適な方法で動作します。
最適化として、コンポーネントから直接フォントなどのアセットを事前読み込みする手動の方法も提供します。現在これらの機能を実装中で、まもなく詳細を共有する予定です。
Document Metadata
アプリの異なるページや画面には、<title>タグ、説明、その画面固有の他の<meta>タグなど、異なるメタデータがある場合があります。保守の観点から、この情報をそのページや画面のReactコンポーネントの近くに保持する方がスケーラブルです。
しかし、このメタデータのHTMLタグは、通常アプリの最上位のコンポーネントでレンダリングされるドキュメントの<head>内にある必要があります。
現在、人々はこの問題を2つの技術のいずれかで解決しています。1つの技術は、その中の<title>、<meta>、その他のタグをドキュメントの<head>に移動する特別なサードパーティコンポーネントをレンダリングすることです。これは主要なブラウザでは機能しますが、Open Graphパーサーなど、クライアントサイドJavaScriptを実行しない多くのクライアントがあるため、この技術は普遍的に適用できません。
別の技術は、ページを2つの部分でサーバーレンダリングすることです。まず、メインコンテンツがレンダリングされ、すべてのそのようなタグが収集されます。次に、これらのタグで<head>がレンダリングされます。最後に、<head>とメインコンテンツがブラウザに送信されます。このアプローチは機能しますが、<head>を送信する前にすべてのコンテンツのレンダリングを待つ必要があるため、React 18のStreaming Server Rendererを活用することができません。
これが、コンポーネントツリーのどこでも<title>、<meta>、メタデータ<link>タグをレンダリングするための組み込みサポートを追加している理由です。完全にクライアントサイドのコード、SSR、将来的にはRSCを含むすべての環境で同じように動作します。まもなくこれについての詳細を共有します。
React Optimizing Compiler
前回の更新以来、React用の最適化コンパイラであるReact Forgetの設計を積極的に反復してきました。以前は「自動メモ化コンパイラ」として話していましたが、それはある意味では正しいです。しかし、コンパイラを構築することで、Reactのプログラミングモデルをさらに深く理解することができました。
React Forgetをより良く理解する方法は、自動リアクティビティコンパイラとしてです。Reactの核心的なアイデアは、開発者が現在の状態の関数としてUIを定義することです。プレーンなJavaScript値(数値、文字列、配列、オブジェクト)を扱い、標準的なJavaScriptイディオム(if/else、forなど)を使用してコンポーネントロジックを記述します。メンタルモデルは、アプリケーションの状態が変更されるたびに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内のリアクティビティシステムを作成しています。
Offscreen Rendering
Offscreen renderingは、追加のパフォーマンスオーバーヘッドなしにバックグラウンドで画面をレンダリングするReactの今後の機能です。DOM要素だけでなくReactコンポーネントでも動作するcontent-visibility CSSプロパティのバージョンと考えることができます。
研究中に、さまざまなユースケースを発見しました:
- ルーターがバックグラウンドで画面を事前レンダリングして、ユーザーがそれらにナビゲートしたときに即座に利用できるようにする
- タブ切り替えコンポーネントが非表示のタブの状態を保持して、ユーザーが進行状況を失うことなくそれらの間を切り替えられるようにする
- 仮想化リストコンポーネントが表示ウィンドウの上下に追加の行を事前レンダリングする
- モーダルやポップアップを開くときに、モーダル以外のすべてのイベントと更新が無効になるように、アプリの残りの部分を「バックグラウンド」モードにする
ほとんどのReact開発者は、ReactのoffscreenAPIと直接やり取りすることはありません。代わりに、offscreen renderingはルーターやUIライブラリなどに統合され、それらのライブラリを使用する開発者は追加の作業なしに自動的に恩恵を受けます。
アイデアは、コンポーネントの書き方を変更することなく、任意のReactツリーをオフスクリーンでレンダリングできるようにすることです。コンポーネントがオフスクリーンでレンダリングされるとき