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

Hermesのデフォルト化に向けて

·14分で読めます
Xuan Huang
Xuan Huang
ソフトウェアエンジニア @ Meta

2019 年に Hermes を発表して以来、コミュニティでの採用が増加しています。React Native アプリの人気のあるメタフレームワークを維持している Expo のチームは、最近、Expo の最も要望の多かった機能の 1 つであった後、Hermes の実験的な サポートを 発表しました。人気のあるモバイルデータベースである Realm のチームも、最近 Hermes のアルファサポートをリリースしました。この投稿では、Hermes を React Native の最高の JavaScript エンジンにするために、過去 2 年間に達成した最もエキサイティングな進歩のいくつかを紹介したいと思います。今後、これらの改善とさらなる改善により、Hermes をすべてのプラットフォームで React Native のデフォルトの JavaScript エンジンにできると確信しています。

React Native の最適化

Hermes の決定的な特徴は、コンパイル作業を事前に行う方法であり、Hermes を有効にした React Native アプリは、プレーンな JavaScript ソースではなく、事前コンパイルされた最適化されたバイトコードで出荷されます。これにより、ユーザーのために製品を起動するために必要な作業量が劇的に削減されます。Facebook とコミュニティアプリの両方からの測定結果は、Hermes を有効にすると、製品の TTI (Time-To-Interactive) メトリックがほぼ半分に削減されることが多いことを示唆しています。

とはいえ、React Native に特化した JavaScript エンジンとして Hermes をさらに改善するために、他の多くの側面で Hermes の改善に取り組んできました。

Fabric 向けの新しいガベージコレクターの構築

新しい React Native アーキテクチャで今後の Fabric レンダラーにより、UI スレッドで JavaScript を同期的に呼び出すことが可能になります。ただし、これは、JavaScript スレッドの実行に時間がかかりすぎると、目立つ UI フレームのドロップやユーザー入力のブロックを引き起こす可能性があることを意味します。React Fiber によって有効になる同時レンダリングは、レンダリング作業をチャンクに分割することで、長い JavaScript タスクのスケジューリングを回避します。しかし、JavaScript スレッドからのレイテンシのもう 1 つの一般的な原因は、JavaScript エンジンがガベージコレクション (GC) を実行するために「世界を停止」する必要がある場合です。

Hermes の以前のデフォルトのガベージコレクターである GenGC は、シングルスレッドの世代別ガベージコレクターでした。新しい世代は典型的なセミスペースコピー戦略を使用し、古い世代はマークコンパクト戦略を使用して、メモリを積極的にオペレーティングシステムに返すのに非常に優れています。シングルスレッドのため、GenGC には長い GC ポーズを引き起こすという欠点があります。Facebook for Android のような複雑なアプリでは、平均 200ms のポーズ、または p99 で 1.4 秒のポーズを観測しました。Facebook for Android の大規模で多様なユーザーベースを考慮すると、7 秒もの長さになったこともあります。

これを軽減するために、Hades と呼ばれるまったく新しいほとんど同時並行の GC を実装しました。Hades は、GenGC とまったく同じように若い世代を収集しますが、スナップショットアットザビギニングスタイルのマークスイープコレクターで古い世代を管理します。これにより、エンジンメインスレッドが JavaScript コードを実行するのをブロックすることなく、ほとんどの作業をバックグラウンドスレッドで実行することで、GC ポーズ時間を大幅に短縮できます。当社の統計によると、Hades は 64 ビットデバイスでは p99.9 で 48ms しか停止しません (GenGC よりも 34 倍高速!)、32 ビットデバイスでは p99.9 で約 88ms (シングルスレッドのインクリメンタル GC として動作します)。これらのポーズ時間の改善は、より高価な書き込みバリア、遅いフリーリストベースの割り当て (バンプポインターアロケーターとは対照的に)、およびヒープの断片化の増加の必要性により、全体的なスループットを犠牲にする可能性があります。これらは適切なトレードオフであると考えており、結合や追加のメモリ最適化 (これについては後述します) を介して全体的に低いメモリ消費を達成することができました。

パフォーマンスの痛点への対応

アプリケーションの起動時間は多くのアプリの成功にとって重要であり、React Native の境界を継続的に押し広げています。Hermes に実装する新しい JavaScript 機能ごとに、本番環境のパフォーマンスへの影響を慎重に監視し、メトリックが低下しないようにしています。Facebook では、現在、Metro の Hermes 用の専用 Babel トランスフォームプロファイルを実験しており、数十の Babel トランスフォームを Hermes のネイティブ ESNext 実装に置き換えています。多くの画面で18-25% の TTI 改善全体的なバイトコードサイズの減少を観測でき、OSS でも同様の結果が期待できます。

起動パフォーマンスに加えて、React Native アプリ、特に仮想現実のメモリフットプリントを改善の機会として特定しました。JavaScript エンジンとして私たちが持っている低レベルの制御のおかげで、ビットとバイトを絞り出すことで一連のメモリ最適化を実現することができました

  1. 以前は、すべての JavaScript 値は、64 ビットアーキテクチャで浮動小数点ダブルとポインターを表すために、64 ビット NaN ボックスエンコードされたタグ付き値として表現されていました。しかし、ほとんどの数値は小さい整数 (SMI) であり、クライアントサイドアプリケーションの JavaScript ヒープは一般的に 4GiB を超えることはないため、これは実際には無駄です。これを解決するために、新しい 32 ビットエンコーディングを導入しました。このエンコーディングでは、SMI とポインターは 29 ビットでエンコードされ (ポインターは 8 バイト境界に配置されるため、下位 3 ビットは常にゼロであると仮定できます)、残りの JS 数値はヒープにボックス化されます。これにより、JavaScript ヒープサイズが約 30% 削減されました。
  2. 異なる種類の JavaScript オブジェクトは、JavaScript ヒープ内で異なる種類の GC 管理セルとして表現されます。これらのセルのヘッダーのメモリレイアウトを積極的に最適化することで、メモリ使用量をさらに約 15% 削減できます

Hermes の主要な決定の 1 つは、ジャストインタイム (JIT) コンパイラを実装しないことでした。これは、ほとんどの React Native アプリでは、追加のウォームアップコストとバイナリおよびメモリへの追加フットプリントが実際には価値がないと信じているためです。長年にわたり、React Native ワークロードで Hermes のスループットを他のエンジンと競合させるために、インタープリターのパフォーマンスとコンパイラの最適化に多大な努力を注いできました。私たちは、あらゆる場所 (インタープリターディスパッチループ、スタックレイアウト、オブジェクトモデル、GC など) からパフォーマンスのボトルネックを特定することで、スループットの向上に引き続き注力しています。今後のリリースでさらにいくつかの数字を期待してください!

垂直統合の開拓

Facebook では、大規模なモノリポジトリ内でプロジェクトを併置することを好みます。エンジン (Hermes) とホスト (React Native) が密接に連携することで、垂直統合のための多くの余地が生まれました。いくつか例を挙げると

  • Hermes は Chrome DevTools プロトコルを話すことで、Chrome デバッガーによるデバイス上 JavaScript デバッグをサポートしています。同期ネイティブ呼び出しのデバッグをサポートし、一貫したランタイム環境を保証するため、従来の「リモート JS デバッグ」(アプリ内プロキシを使用してデスクトップ Chrome で JS を実行する) よりも優れています。React DevTools、Metro、Inspector などとともに、Hermes デバッガーは現在 Flipper の一部であり、ワンストップの開発者エクスペリエンスを提供しています。
  • React Native アプリの初期化パス中に割り当てられるオブジェクトは、多くの場合長期間存在し、世代別 GC が利用する世代仮説に従いません。したがって、GC ポーズのトリガーと TTI の遅延を回避するために、最初の 32MiB を直接古い世代に割り当てるように (プリテニュアリングとして知られています) React Native で Hermes を構成しました
  • 新しい React Native アーキテクチャは、JavaScript エンジンを C++ プログラムに組み込むための軽量で汎用的な API である JSI (JavaScript インターフェース) に大きく基づいています。JS エンジンを維持するチームが JSI API 実装も維持することで、信頼性、パフォーマンス、Facebook の規模で実証済みの最高の統合を提供できると確信しています。
  • JavaScript の並行処理プリミティブ (例: promises) とプラットフォームの並行処理プリミティブ (例: microtasks) の両方をセマンティックに正しく、かつ高性能にすることは、React の同時レンダリングと React Native アプリの将来にとって非常に重要です。歴史的に、React Native の promise は、標準化されていない setImmediate API を使用してポリフィルされていました。JS エンジンからのネイティブの promise と microtask を JSI を介して利用できるようにし、最新の非同期 JavaScript コードをより適切にサポートするために、Web 標準に最近追加された queueMicrotask をプラットフォームに導入する作業を進めています。

コミュニティ全体の取り込み

Hermes は Facebook で非常に素晴らしい成果を上げてきました。しかし、コミュニティが Hermes を使用してエコシステム全体でエクスペリエンスを強化し、誰もがそのすべての機能を利用し、その可能性を最大限に引き出すことができるようになるまで、私たちの作業は完了しません。

新しいプラットフォームへの拡大

Hermes は当初、Android 上の React Native のみでオープンソース化されました。それ以来、コミュニティのメンバーが Hermes のサポートを React Native のエコシステムが拡大した他の多くのプラットフォームに拡大しているのを見て、私たちは興奮しています。

Callstack は、React Native 0.64 で Hermes を iOS に導入する取り組みを主導しました。彼らは 一連の記事を書き、それをどのように達成したかについて ポッドキャストをホストしました。彼らのベンチマークによると、Hermes は Mattermost アプリの iOS で、JSC と比較して起動時に約 40% の改善と約 18% のメモリ削減を一貫して提供でき、アプリサイズのオーバーヘッドはわずか 2.4 MiB でした。実際に自分の目で見てみることをお勧めします

Microsoft は React Native for Windows および macOS に Hermes を導入しています。Microsoft Build 2020 で、Microsoft は、Hermes のメモリ影響 (ワーキングセット) が React Native for Windows の Chakra エンジンよりも 13% 低いと共有しました。最近、いくつかの合成ベンチマークでは、Hermes 0.8 (Hades と前述の SMI およびポインター圧縮最適化が付属) が他のエンジンよりも 30% ~ 40% 少ないメモリを使用することがわかりました。驚くことではありませんが、React Native 上に構築されたデスクトップ版 Messenger のビデオ通話エクスペリエンスも Hermes を搭載しています。

最後に、Oculus Home を含む Oculus で React ファミリーのテクノロジーで構築されたすべての仮想現実体験も Hermes を搭載しています。

コミュニティのサポート

コミュニティの一部が Hermes を採用するのを妨げるブロッカーがまだあることを認識しており、これらの不足している機能のサポートを構築することに取り組んでいます。私たちの目標は、Hermes がほとんどの React Native アプリにとって適切な選択となるように、すべての機能を完全に備えることです。コミュニティが Hermes のロードマップをどのように形成してきたかを次に示します。

  • ProxyReflect は、Facebook が使用していなかったため、当初 Hermes から除外されていました。Proxy を追加すると、Proxy が使用されていない場合でもプロパティルックアップパフォーマンスが低下するのではないかと懸念していました。しかし、Proxy は MobXImmer などの人気ライブラリのために、すぐに Hermes の最も要望の多かった機能になりました。私たちは慎重に評価し、コミュニティのためだけに構築することに決定し、非常に低いコストで実装することができました。これは私たちが使用しない機能であるため、その安定性を証明するためにコミュニティに依存しました。まず、フラグの背後で Proxy をテストし、リリース v0.4 および v0.5 用のオプトイン npm パッケージを作成しました。そして、v0.7 からデフォルトで有効になっています
  • ECMAScript Internationalization API Specification (ECMA-402, または Intl) は、2 番目に要望の多かった機能でした。Intl は膨大な API セットであり、多くの場合、実装には6MB 相当Unicode CLDR データを含める必要があります。これが、FormatJS (別名 react-intl) のようなポリフィルや、コミュニティ JSC の国際化対応ビルドのような JS エンジンが非常に巨大である理由です。Hermes のバイナリサイズを大幅に増やさないように、私たちは、オペレーティングシステムに含まれるライブラリによって提供される ICU 機能を利用してマッピングするという別の戦略で実装することにしました。ただし、プラットフォーム間で動作にいくつかの (多くの場合軽微な) ばらつきが生じるという代償を伴います。
  • コミュニティに影響を与える問題を特定するために私たちと協力してくださった皆様に感謝いたします。

まとめ

まとめると、私たちのビジョンは、Hermes をすべての React Native プラットフォームでデフォルトの JavaScript エンジンにする準備をすることです。私たちはすでにそれに向けて取り組んでおり、この方向性について皆様からのご意見を伺いたいと思っています。

円滑な採用のためにエコシステムを準備することは、私たちにとって非常に重要です。Hermes を試してみて、フィードバック、質問、機能リクエスト、非互換性については、GitHub リポジトリに問題を報告することをお勧めします。

謝辞

Hermes の改善に貢献してくださった Hermes チーム、React Native チーム、そして React Native コミュニティの多くの貢献者に心から感謝いたします。

また、執筆中にご協力いただいた Eli White、Luna Wei、Neil Dhar、Tim Yung、Tzvetan Mikov、その他多くの方々に個人的に感謝したいと思います (アルファベット順)。