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

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

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

React Nativeは、Facebookファミリーの複数のアプリの様々な場所で使用されており、主要なFacebookアプリのトップレベルタブでも利用されています。この投稿では、特に注目度の高い製品であるマーケットプレイスに焦点を当てます。これは十数カ国で利用可能で、ユーザーは他のユーザーが提供する製品やサービスを発見できます。

2017年前半、Relayチーム、Marketplaceチーム、モバイルJSプラットフォームチーム、React Nativeチームの共同努力により、Android Year Class 2010-11デバイスにおけるMarketplaceのTime to Interaction (TTI) を半減させました。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) Marketplaceがニュースフィードや他のトップレベルタブなど、多数の複雑なビューと共存していること、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%削減しました。Marketplaceの起動と他の製品との競合を排除することで、起動を同等の時間短縮しました。

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

結論

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

これまでの教訓は、「測定、測定、測定!」です。Relay ModernやLazy NativeModulesのように、実行時コストをビルド時コストに移行することで得られる成果もあります。また、コードの並列化やデッドコードの削除をより賢く行うことで、作業を回避することから得られる成果もあります。そして、スレッドブロックのクリーンアップなど、React Nativeへの大規模なアーキテクチャ変更から得られる成果もあります。パフォーマンスに壮大な解決策はなく、長期的なパフォーマンスの向上は、漸進的な計測と改善から生まれます。認知バイアスがあなたの意思決定に影響を与えないようにしましょう。代わりに、慎重に本番データを収集し、解釈して、将来の作業を導きましょう。

今後の計画

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