本文へスキップ

マーケットプレイスにおけるReact Nativeのパフォーマンス

5分間の読書
Facebookソフトウェアエンジニア

React Nativeは、メインのFacebookアプリのトップレベルタブを含む、Facebookファミリーの複数のアプリの複数の場所で利用されています。この記事では、非常に目立つ製品であるマーケットプレイスに焦点を当てます。これは10数カ国で利用可能であり、ユーザーは他のユーザーが提供する製品やサービスを発見できます。

2017年前半、Relayチーム、マーケットプレイスチーム、モバイルJSプラットフォームチーム、そしてReact Nativeチームの共同作業により、Androidの2010-11年クラスのデバイスにおけるマーケットプレイスの対話開始時間(TTI)を半分に短縮しました。Facebookは、これらのデバイスを低価格帯のAndroidデバイスと見なしており、どのプラットフォームやデバイスタイプよりもTTIが最も遅いデバイスです。

一般的なReact Nativeの起動は、次のようになります。

免責事項:比率は代表的なものではなく、React Nativeの構成と使用方法によって異なります。

最初にReact Nativeコア(別名「ブリッジ」)を初期化してから、製品固有のJavaScriptを実行します。これにより、React Nativeがネイティブ処理時間内にレンダリングするネイティブビューが決まります。

異なるアプローチ

初期の過ちの1つは、SystraceとCTScanをパフォーマンス改善の取り組みの推進力としてしまったことです。これらのツールは2016年に多くの容易な改善を見つけるのに役立ちましたが、SystraceとCTScanのどちらも**本番環境のシナリオを表していない**こと、そして現実世界で起こることをエミュレートできないことを発見しました。時間配分の比率はしばしば不正確であり、時には大きく外れていました。極端な例では、数ミリ秒かかるはずのものが、実際には数百または数千ミリ秒かかるものもありました。とはいえ、CTScanは有用であり、本番環境に到達する前に3分の1の回帰を検出することが分かりました。

Androidでは、これらのツールの欠点は、1)React Nativeが多スレッドフレームワークであること、2)マーケットプレイスがニュースフィードやその他のトップレベルタブなどの多くの複雑なビューと共存していること、3)計算時間が大きく変動すること、に起因すると考えています。したがって、この半年間は、本番環境での測定と分析を、意思決定と優先順位付けのほぼすべての基礎にしました。

本番環境での計測の道

本番環境での計測は一見簡単そうに見えますが、実際には非常に複雑なプロセスであることが分かりました。マスターへのコミットの反映、アプリのPlayストアへのプッシュ、作業に自信を持つのに十分な本番サンプルの収集という遅延のために、それぞれ2~3週間の複数のイテレーションサイクルが必要でした。各イテレーションサイクルには、分析が正確かどうか、適切な粒度を持つか、そして全体の時間範囲に正しく合計されるかどうかを確認することが含まれていました。アルファ版とベータ版リリースは一般ユーザーを表していないため、それらに依存することはできませんでした。本質的に、私たちは数百万ものサンプルの集計に基づいて、非常に正確な本番環境のトレースを非常に時間をかけて構築しました。

分析における各ミリ秒が親メトリクスに正しく合計されることを綿密に検証した理由の1つは、初期の段階で計測にギャップがあることに気づいたからです。初期の分析では、スレッドジャンプによって発生するストールが考慮されていませんでした。スレッドジャンプ自体は高価ではありませんが、既に作業を行っているビジーなスレッドへのスレッドジャンプは非常に高価です。最終的に、適切なタイミングで`Thread.sleep()`呼び出しを散らすことで、これらのブロックをローカルで再現し、次の方法で修正することができました。

  1. AsyncTaskへの依存を削除すること、
  2. UIスレッドでのReactContextとNativeModulesの強制初期化を元に戻すこと、そして
  3. 初期化時のReactRootViewの測定への依存を削除すること。

これらのスレッドブロッキングの問題を解決することで、起動時間を25%以上削減できました。

本番環境のメトリクスは、これまでのいくつかの仮定にも異議を唱えました。たとえば、モジュールを1つのバンドルにまとめることで初期化コストを削減するという仮定の下、多くのJavaScriptモジュールを起動パスでプリロードしていました。しかし、これらのモジュールをプリロードおよび共存させるコストは、そのメリットをはるかに上回っていました。インラインrequireブラックリストの再構成と起動パスからのJavaScriptモジュールの削除により、Relay Classic(Relay Modernのみが必要な場合)などの不要なモジュールの読み込みを回避できました。現在、私たちの`RUN_JS_BUNDLE`分析は75%以上高速です。

また、製品固有のネイティブモジュールの調査によっても成果を得ることができました。たとえば、ネイティブモジュールの依存関係を遅延注入することで、そのネイティブモジュールのコストを98%削減しました。マーケットプレイスの起動と他の製品との競合を排除することで、同等の期間だけ起動時間を短縮しました。

最も良い点は、これらの改善の多くが、React Nativeで構築されたすべての画面に広く適用できることです。

結論

React Nativeの起動パフォーマンスの問題は、JavaScriptが遅い、またはネットワーク時間が非常に長いことが原因であると多くの人が考えています。JavaScriptの高速化はTTIを無視できないほど削減しますが、これらは以前考えられていたよりもTTIに占める割合がはるかに小さいです。

これまでの教訓は、*計測、計測、計測!*です。Relay ModernやLazy NativeModulesなど、実行時コストをビルド時に移行することによって、成果が得られます。他の成果は、コードを並列化したり、デッドコードを削除したりすることで作業を回避することによって得られます。そして、スレッドブロッキングの解消など、React Nativeの大規模なアーキテクチャ変更から成果が得られることもあります。パフォーマンスに対する究極の解決策はなく、長期的なパフォーマンスの向上は、段階的な計測と改善から得られるでしょう。認知バイアスに影響されないようにしましょう。代わりに、本番環境のデータを注意深く収集して解釈し、今後の作業を導きましょう。

今後の計画

長期的な目標としては、マーケットプレイスのTTIをネイティブで構築された同様の製品と同等にすること、そして一般的にReact Nativeのパフォーマンスをネイティブのパフォーマンスと同等にすることです。さらに、この半年間でブリッジの起動コストを約80%削減しましたが、Prepackなどのプロジェクトや、より多くのビルド時間処理を通じて、React Nativeブリッジのコストをゼロに近づける計画です。