メインコンテンツへスキップ

MarketplaceでのReact Nativeパフォーマンス

·6分で読めます
Facebook ソフトウェアエンジニア

React Nativeは、Facebookファミリーの複数のアプリの様々な場所で使われており、これにはメインのFacebookアプリのトップレベルタブも含まれます。この記事では、特に注目度の高いプロダクトであるマーケットプレイスに焦点を当てます。これは十数カ国で利用可能で、ユーザーが他のユーザーから提供される商品やサービスを見つけることができます。

2017年前半、Relayチーム、マーケットプレイスチーム、モバイルJSプラットフォームチーム、そしてReact Nativeチームの共同の取り組みにより、AndroidのYear Class 2010-11デバイスにおいて、マーケットプレイスのインタラクション可能になるまでの時間 (TTI: Time to Interaction) を半減させました。Facebookは歴史的にこれらのデバイスをローエンドのAndroidデバイスと見なしており、どのプラットフォームやデバイスタイプの中でもTTIが最も遅いものでした。

典型的なReact Nativeの起動処理は次のようになります。

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

まず、React Nativeコア(別名「ブリッジ」)を初期化し、その後プロダクト固有のJavaScriptを実行します。このJavaScriptが、ネイティブ処理時間内にReact Nativeがどのネイティブビューをレンダリングするかを決定します。

異なるアプローチ

私たちが犯した初期の過ちの一つは、SystraceとCTScanにパフォーマンス改善の取り組みを委ねてしまったことでした。これらのツールは2016年に多くの簡単な改善点を見つけるのに役立ちましたが、SystraceとCTScanはどちらも**本番環境のシナリオを代表するものではなく**、実際の環境で何が起こるかをエミュレートできないことがわかりました。各項目に費やされる時間の比率はしばしば不正確で、時には大きく外れていました。極端な例では、数ミリ秒かかると予想していたものが、実際には数百、数千ミリ秒かかっていました。とはいえ、CTScanは有用であり、リグレッションの3分の1を本番環境に到達する前に発見しています。

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

本番環境の計測への道

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

各項目のミリ秒が親のメトリクスに正しく合計されることを細かく検証した理由の一つは、早い段階で計測にギャップがあることに気づいたからです。当初の分析では、スレッドの切り替えによって引き起こされる遅延が考慮されていませんでした。スレッドの切り替え自体は高コストではありませんが、既に作業中のビジーなスレッドへの切り替えは非常に高コストです。最終的に、適切なタイミングで`Thread.sleep()`を呼び出すことでこれらのブロッキングをローカルで再現し、以下の方法で修正することができました。

  1. AsyncTaskへの依存を削除
  2. UIスレッドでのReactContextとNativeModulesの強制的な初期化を取りやめ
  3. 初期化時にReactRootViewを測定する依存関係を削除

これらのスレッドブロッキングの問題を合わせて取り除くことで、起動時間が25%以上短縮されました。

本番環境のメトリクスは、以前のいくつかの仮説にも疑問を投げかけました。たとえば、以前は多くのJavaScriptモジュールを起動時にプリロードしていました。これは、モジュールを1つのバンドルにまとめることで初期化コストが削減されるという仮定に基づいたものです。しかし、これらのモジュールをプリロードし、まとめるコストは、その利点をはるかに上回っていました。inline 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ブリッジのコストをゼロに近づける計画です。