本日、Rustベースの新しいRelayコンパイラーのプレビュー版をオープンソースとして(v13.0.0-rc.1
として)リリースできることを大変嬉しく思います!この新しいコンパイラーは、より高速で、新しいランタイム機能をサポートし、将来のさらなる成長のための強力な基盤を提供します。
このリリースに至るまで、Metaのコードベースは増加の一途をたどっていました。私たちの規模では、コードベース内のすべてのクエリをコンパイルするのにかかる時間は、開発者の生産性を直接犠牲にして増加していました。JavaScriptベースのコンパイラーを最適化するために多くの戦略を試みましたが(以下で説明します)、パフォーマンス向上を段階的に実現する能力は、コードベース内のクエリの数の増加に追いつくことができませんでした。
そこで、コンパイラーをRustで書き直すことにしました。Rustを選んだ理由は、高速でメモリセーフであり、スレッド間で大きなデータ構造を安全に共有することが容易だからです。開発は2020年初頭に開始され、コンパイラーは同年末に社内でリリースされました。ロールアウトはスムーズで、アプリケーション開発の中断はありませんでした。初期の社内ベンチマークでは、コンパイラーのパフォーマンスが平均で約5倍、P95で約7倍向上したことが示されました。それ以来、コンパイラーのパフォーマンスはさらに向上しています。
この記事では、Relayにコンパイラーがある理由、新しいコンパイラーで実現したいこと、その新機能、そしてRust言語を選んだ理由について説明します。新しいコンパイラーの使用を開始するには、コンパイラーパッケージのREADMEまたはリリースノートをご覧ください。
Relayにコンパイラーがあるのはなぜですか?
Relayには、安定性の保証と優れたランタイムパフォーマンスを実現するためにコンパイラーがあります。
その理由を理解するために、フレームワークを使用するワークフローを考えてみましょう。Relayでは、開発者はGraphQLと呼ばれる宣言型言語を使用して、各コンポーネントが必要とするデータを指定しますが、その取得方法は指定しません。コンパイラーは、これらのコンポーネントのデータ依存関係を、特定のページのすべてのデータをフェッチするクエリにまとめ、Relayアプリケーションに高いレベルのパフォーマンスと安定性を与えるアーティファクトを事前に計算します。
このワークフローでは、コンパイラーは
- コンポーネントを個別に推論できるようにすることで、大きなクラスのバグを不可能にし、
- 可能な限り多くの作業をビルド時にシフトすることで、Relayを使用するアプリケーションのランタイムパフォーマンスを大幅に向上させます。
それぞれについて順番に見ていきましょう。
ローカル推論のサポート
Relayでは、コンポーネントはGraphQLフラグメントを使用して独自のデータ要件のみを指定します。コンパイラーは、これらのコンポーネントのデータ依存関係を、特定のページのすべてのデータをフェッチするクエリにまとめます。開発者は、データの依存関係がより大きなクエリにどのように適合するかを心配することなく、コンポーネントの作成に集中できます。
しかし、Relayはこのローカル推論をさらに一歩進めています。コンパイラーは、Relayランタイムが特定のコンポーネントのフラグメントによって選択されたデータのみを読み取るために使用するファイルも生成します(これをデータマスキングと呼びます)。そのため、コンポーネントは、明示的に要求していないデータに(実際には、型レベルだけでなく!)アクセスすることはありません。
したがって、あるコンポーネントのデータ依存関係を変更しても、別のコンポーネントが表示するデータに影響を与えることはできません。つまり、**開発者はコンポーネントを個別に推論できます。**これにより、Relayアプリは比類のないレベルの安定性を実現し、大きなクラスのバグを不可能にします。また、Relayが同じコードベースに多くの開発者が触れることができる主な理由の1つです。
ランタイムパフォーマンスの向上
Relayはコンパイラーを利用して、可能な限り多くの作業をビルド時にシフトすることで、Relayアプリのパフォーマンスを向上させます。
Relayコンパイラーは、すべてのコンポーネントのデータ依存関係をグローバルに把握しているため、手書きで記述した場合よりも優れたクエリを記述できます。これは、ランタイムでは実行不可能なほど遅い方法でクエリを最適化することで実現できます。たとえば、生成されたクエリからアクセスできないブランチを削除し、クエリの同一セクションをフラット化します。
また、これらのクエリはビルド時に生成されるため、Relayアプリケーションは、GraphQLフラグメントから抽象構文木(AST)を生成したり、それらのASTを操作したり、ランタイムにクエリテキストを生成したりすることはありません。代わりに、Relayコンパイラーは、アプリケーションのGraphQLフラグメントを、ネットワークデータをストアに書き込み、そこから読み取る方法を記述する、事前に計算され最適化された命令(普通のJavascriptデータ構造として)に置き換えます。
この配置の追加の利点として、Relayアプリケーションバンドルには、スキーマも、永続クエリを使用している場合はGraphQLフラグメントの文字列表現も含まれていません。これにより、アプリケーションのサイズが削減され、ユーザーの帯域幅が節約され、アプリケーションのパフォーマンスが向上します。
実際、新しいコンパイラーはさらに進んで、別の方法でユーザーの帯域幅を節約します。Relayは、ビルド時に各クエリテキストについてアプリケーションのサーバーに通知し、一意のクエリIDを生成できます。つまり、アプリケーションは、非常に長い可能性のあるクエリ文字列をユーザーの低速ネットワーク経由で送信する必要がなくなります。このような永続クエリを使用する場合、ネットワークリクエストを行うためにネットワーク経由で送信する必要があるのは、クエリIDとクエリ変数だけです。
新しいコンパイラーは何を実現しますか?
コンパイル言語は、動的言語と比較して、摩擦を生じさせ、開発者の速度を低下させると認識されることがあります。ただし、Relayはコンパイラーを利用して摩擦を減らし、一般的な開発タスクを容易にします。たとえば、Relayは、ページネーションや新しい変数を使用したクエリの再フェッチなど、微妙に間違えやすい一般的なインタラクションのための高レベルのプリミティブを公開しています。
これらのインタラクションに共通しているのは、古いクエリから新しいクエリを生成する必要があるため、ボイラープレートと重複が発生することです。これは自動化の理想的なターゲットです。Relayは、コンパイラーのグローバルな知識を活用して、開発者が1つのディレクティブを追加し、1つの関数呼び出しを変更するだけで、ページネーションと再フェッチを有効にできるようにします。それだけです。
**しかし、開発者が簡単にページネーションを追加できるようにすることは、氷山の一角にすぎません。**コンパイラーに対する私たちのビジョンは、機能を提供し、ボイラープレートを回避するための、より高レベルのツールを提供し、開発者にリアルタイムの支援と洞察を提供し、GraphQLで作業するための他のツールで使用できる部品で構成されることです。
このプロジェクトの主な目標は、書き直されたコンパイラーのアーキテクチャが、今後数年間でこのビジョンを達成できるようにすることでした。
まだそこに到達していませんが、各基準で大きな成果を上げています。
たとえば、新しいコンパイラーには、新しい@required
ディレクティブのサポートが付属しています。これにより、特定のサブフィールドが読み取られるときにnullの場合、親のリンクフィールドがnullに設定されるか、エラーがスローされます。これは些細な生活の質の向上のように聞こえるかもしれませんが、コンポーネントのコードの半分がnullチェックである場合、@required
はかなり良く見え始めます。
@required
のないコンポーネント
@required
あり
次に、コンパイラーは、入力時にフィールド名を自動補完し、ホバー時に型情報を表示するなど、多くの機能を備えた社内専用のVSCode拡張機能を強化します。まだ公開していませんが、いつか公開したいと思っています。私たちの経験では、このVSCode拡張機能により、GraphQLデータの操作がはるかに簡単かつ直感的になります。
最後に、新しいコンパイラーは、他のGraphQLツールで再利用できる一連の独立したモジュールとして記述されました。これをRelayコンパイラープラットフォームと呼びます。社内では、これらのモジュールは、他のコード生成ツールや、異なるプラットフォーム向けの他のGraphQLクライアントで再利用されています。
コンパイラーのパフォーマンス
これまでは、Relayにコンパイラーがある理由と、書き直しで何が実現できるかについて説明しました。しかし、2020年にコンパイラーを書き直すことにした理由、つまりパフォーマンスについては説明していません。
コンパイラーを書き直すことを決定する前、コードベース内のすべてのクエリをコンパイルするのにかかる時間は、コードベースの成長に伴い、徐々に、しかし容赦なく遅くなっていました。パフォーマンス向上を実現する能力は、コードベース内のクエリの数の増加に追いつくことができず、この窮地から抜け出すための段階的な方法が見つかりませんでした。
JavaScriptの限界に達する
以前のコンパイラーはJavaScriptで記述されていました。これは、いくつかの理由から自然な言語の選択でした。それは、私たちのチームが最も経験豊富な言語であり、Relayランタイムが記述された言語(コンパイラーとランタイム間でコードを共有できる)、GraphQLリファレンス実装とモバイルGraphQLツールが記述された言語でした。
コンパイラーのパフォーマンスはかなり長い間妥当なものでした。Node / V8には高度に最適化されたJITコンパイラーとガベージコレクターが付属しており、注意すれば非常に高速になります(私たちはそうでした)。しかし、コンパイル時間は増加していました。
追いつくために多くの戦略を試みました
- コンパイラーを増分的にしました。変更に応じて、その変更の影響を受けた依存関係のみを再コンパイルしました。
- どの変換が遅いのか(つまり、フラット化)を特定し、可能な限りのアルゴリズムの改善(メモ化の追加など)を行いました。
- 公式の
graphql
npmパッケージのGraphQLスキーマ表現は、スキーマを表すために数ギガバイトのメモリを必要としたため、カスタムフォークに置き換えました。 - 最もホットなコードパスで、プロファイラーガイドのマイクロ最適化を行いました。たとえば、オブジェクトを複製および変更するために
...
演算子を使用するのをやめ、オブジェクトをコピーするときにオブジェクトのプロパティを明示的にリストすることを好みました。これにより、オブジェクトの隠しクラスが保持され、コードをJITで最適化することができました。 - コンパイラを再構築し、複数のワーカーに処理を分散するようにしました。各ワーカーは1つのスキーマを処理します。複数のスキーマを持つプロジェクトはMeta社以外ではあまり一般的ではないため、ほとんどのユーザーはシングルスレッドのコンパイラを使用していたことになります。
これらの最適化では、社内でのRelayの急速な普及に対応するには不十分でした。
最大の課題は、Node.jsが共有メモリを持つマルチスレッドプログラムをサポートしていないことでした。可能な限り最善の方法は、メッセージの受け渡しによって通信する複数のワーカーを起動することです。
これは、いくつかのシナリオではうまく機能します。たとえば、Jestはこのパターンを採用し、ファイル変換のテストを実行する際にすべてのコアを利用します。Jestはプロセス間で多くのデータやメモリを共有する必要がないため、これは適しています。
一方、私たちのスキーマはメモリ内に複数のインスタンスを持つには大きすぎるため、JavaScriptでスキーマごとに複数スレッドを使用してRelayコンパイラを効率的に並列化する良い方法はありませんでした。
Rustを選択
コンパイラの書き換えを決めた後、プロジェクトのニーズを満たす言語を見つけるために、多くの言語を評価しました。高速で、メモリ安全で、並行性をサポートする言語が求められました。できれば、並行性のバグは実行時ではなく、ビルド時に検出されるものが理想です。同時に、社内で十分にサポートされている言語が必要でした。これにより、選択肢は絞られました。
- C++はほとんどの基準を満たしていましたが、習得が難しいと感じました。また、コンパイラは私たちが望むほど安全性を支援してくれませんでした。
- Javaもおそらく妥当な選択肢だったでしょう。高速でマルチコアですが、低レベルの制御はあまりできません。
- OCamlはコンパイラ分野では実績のある選択肢ですが、マルチスレッディングは困難です。
- Rustは高速で、メモリ安全で、並行性をサポートしています。スレッド間で大きなデータ構造を安全に共有することを容易にします。Rustに対する一般的な期待感、チーム内でのこれまでの経験、そしてFacebookの他のチームによる使用実績から、Rustは明らかに最良の選択肢でした。
社内展開
Rustは素晴らしい選択でした!ほとんどがJavaScript開発者であるチームは、Rustを簡単に導入できました。また、Rustの高度な型システムは、ビルド時に多くのエラーを検出し、高い開発速度を維持するのに役立ちました。
開発は2020年初頭に開始され、同年末にコンパイラを社内で展開しました。初期の社内ベンチマークでは、コンパイラのパフォーマンスが平均で約5倍、P95で約7倍向上したことが示されました。それ以降、コンパイラのパフォーマンスはさらに向上しています。
OSSでのリリース
本日、Relay v13の一部として、新しいバージョンのコンパイラを公開できることを嬉しく思います。新しいコンパイラの機能は以下のとおりです。
@required
ディレクティブ。@no_inline
ディレクティブ。共通のフラグメントのインライン化を防ぎ、生成されるファイルのサイズを小さくするために使用できます。- 競合するGraphQLフィールド、引数、ディレクティブの検証
- TypeScript型生成のサポート
- リモートクエリ永続化のサポート
READMEとリリースノートで、コンパイラに関する詳細情報をご覧いただけます!
私たちは、開発者がグラフ上の派生値にアクセスできるようにすること、ローカルデータを更新するためのより人間工学に基づいた構文のサポートを追加すること、VSCode拡張機能を完全に充実させることなど、コンパイラ内で機能の開発を続けており、これらすべてをオープンソースにリリースしたいと考えています。このリリースを誇りに思っていますが、まだまだ多くのことが控えています!
謝辞
このブログ記事に素晴らしいフィードバックを提供してくれたJoe Savona、Lauren Tan、Jason Bonta、Jordan Eldredgeに感謝します。コンパイラのバグに関連する問題を報告してくれたch1ffa、robrichard、orta、syncに感謝します。TypeScriptのサポートを追加してくれたMaartenStaaに感謝します。現在はデフォルトで有効になっている@required
ディレクティブを有効にすることの難しさを指摘してくれた@andrewingramに感謝します。他にも多くの貢献者がいます。これはまさにコミュニティの努力でした!