Hermesのデフォルト化に向けて
2019年にHermesを発表して以来、コミュニティでの採用がますます進んでいます。React Nativeアプリの人気メタフレームワークを維持しているExpoのチームは最近、Hermesの実験的なサポートを発表しました。これは、Expoで最もリクエストの多かった機能の1つでした。人気のモバイルデータベースであるRealmのチームも最近、Hermesのアルファサポートをリリースしました。この記事では、HermesをReact Nativeのための最高のJavaScriptエンジンにするために、過去2年間で達成した最もエキサイティングな進歩のいくつかを強調したいと思います。今後の展望として、これらの改善やさらなる改善により、HermesをすべてのプラットフォームにわたるReact NativeのデフォルトJavaScriptエンジンにできると確信しています。
React Nativeのための最適化
Hermesの決定的な特徴は、コンパイル作業を事前(ahead-of-time)に行う方法です。つまり、Hermesを有効にしたReact Nativeアプリは、プレーンなJavaScriptソースの代わりに、事前にコンパイルされ最適化されたバイトコードを同梱します。これにより、ユーザーのために製品を起動するために必要な作業量が大幅に削減されます。Facebookとコミュニティアプリの両方からの測定結果によると、Hermesを有効にすると、製品のTTI(Time-To-Interactive)メトリクスがほぼ半分に削減されることが示唆されています。
とは言え、私たちはHermesをReact Nativeに特化したJavaScriptエンジンとしてさらに良くするために、他の多くの側面で改善に取り組んできました。
Fabricのための新しいガベージコレクタの構築
新しいReact NativeアーキテクチャのFabricレンダラーが登場すると、UIスレッドでJavaScriptを同期的に呼び出すことが可能になります。しかし、これはJavaScriptスレッドの実行に時間がかかりすぎると、目に見えるUIフレームのドロップやユーザー入力のブロックを引き起こす可能性があることを意味します。React Fiberによって可能になるコンカレントレンダリングは、レンダリング作業をチャンクに分割することで、長いJavaScriptタスクのスケジューリングを回避します。しかし、JavaScriptスレッドからの遅延の一般的な原因がもう1つあります。それは、JavaScriptエンジンがガベージコレクション(GC)を実行するために「世界を止める」必要があるときです。
Hermesの以前のデフォルトのガベージコレクタであるGenGCは、シングルスレッドの世代別ガベージコレクタでした。新しい世代は典型的なセミスペースコピー戦略を使用し、古い世代はマークコンパクト戦略を使用して、メモリを積極的にオペレーティングシステムに返すのに非常に優れています。シングルスレッドであるため、GenGCには長いGCポーズを引き起こすという欠点があります。Facebook for Androidのような複雑なアプリでは、平均200ミリ秒、p99で1.4秒のポーズが観測されました。Facebook for Androidのユーザーベースが大きく多様であることを考慮すると、最大で7秒にもなることがありました。
これを軽減するために、私たちはHadesという名前の全く新しいほぼコンカレントなGCを実装しました。Hadesは若い世代をGenGCと全く同じように収集しますが、古い世代はsnapshot-at-the-beginningスタイルのマークスイープコレクタで管理します。これにより、エンジンメインスレッドがJavaScriptコードを実行するのをブロックすることなく、バックグラウンドスレッドでほとんどの作業を実行することで、GCのポーズ時間を大幅に短縮できます。私たちの統計によると、Hadesは64ビットデバイスではp99.9でわずか48ミリ秒(GenGCより34倍高速!)しかポーズせず、32ビットデバイス(シングルスレッドのインクリメンタルGCとして動作)ではp99.9で約88ミリ秒です。これらのポーズ時間の改善は、より高価な書き込みバリア、遅いフリーリストベースのアロケーション(バンプポインタアロケータとは対照的)、およびヒープの断片化の増加の必要性のため、全体のスループットを犠牲にする可能性があります。私たちはこれらが正しいトレードオフであると考えており、後述する合体や追加のメモリ最適化を通じて、全体的なメモリ消費量を低減することができました。
パフォーマンスの弱点を突く
アプリケーションの起動時間は多くのアプリの成功に不可欠であり、私たちはReact Nativeの限界を継続的に押し広げています。Hermesに実装する新しいJavaScript機能ごとに、本番環境のパフォーマンスへの影響を注意深く監視し、メトリクスが後退しないようにしています。Facebookでは現在、MetroのHermes専用Babel変換プロファイルを実験しており、十数個のBabel変換をHermesのネイティブESNext実装に置き換えています。多くのサーフェスで18-25%のTTI改善と全体的なバイトコードサイズの減少を観測でき、OSSでも同様の結果を期待しています。
起動パフォーマンスに加えて、特にバーチャルリアリティ向けにReact Nativeアプリのメモリフットプリントが改善の機会であると特定しました。JavaScriptエンジンとして持つ低レベルの制御のおかげで、ビットやバイトを絞り出すことでメモリ最適化を何度も行うことができました。
- 以前は、すべてのJavaScript値は64ビットのNaN-boxingエンコードされたタグ付き値として表現され、64ビットアーキテクチャ上で浮動小数点doubleとポインタを表していました。しかし、ほとんどの数値は小さな整数(SMI)であり、クライアントサイドアプリケーションのJavaScriptヒープは一般的に4GiBを超えることは期待されていないため、これは実際には無駄です。これに対処するため、SMIとポインタを29ビットでエンコードする新しい32ビットエンコーディングを導入しました(ポインタは8バイトアラインされているため、下位3ビットは常にゼロと仮定できます)。残りのJS数値はヒープにボックス化されます。これにより、JavaScriptのヒープサイズが約30%削減されました。
- さまざまな種類のJavaScriptオブジェクトは、JavaScriptヒープ内のさまざまな種類のGC管理セルとして表現されます。これらのセルのヘッダーのメモリレイアウトを積極的に最適化することで、メモリ使用量をさらに約15%削減することができました。
Hermesでの重要な決定の1つは、ジャストインタイム(JIT)コンパイラを実装しないことでした。なぜなら、ほとんどのReact Nativeアプリでは、追加のウォームアップコストやバイナリとメモリのフットプリント増加は実際には価値がないと考えているからです。長年にわたり、私たちはインタプリタのパフォーマンスとコンパイラの最適化に多くの努力を費やし、HermesのスループットをReact Nativeのワークロードで他のエンジンと競合できるようにしました。私たちは、あらゆる場所(インタプリタのディスパッチループ、スタックレイアウト、オブジェクトモデル、GCなど)からパフォーマンスのボトルネックを特定することにより、スループットの向上に引き続き注力しています。今後のリリースで、さらなる数値にご期待ください!
垂直統合の先駆者となる
Facebookでは、大規模なモノレポ内にプロジェクトを同居させることを好みます。エンジン(Hermes)とホスト(React Native)を密接に連携させることで、垂直統合のための多くの余地が生まれました。いくつか例を挙げると
- Hermesは、Chrome DevTools Protocolを話すことで、Chromeデバッガを使用したデバイス上のJavaScriptデバッグをサポートしています。これは、従来の「Remote JS Debugging」(デスクトップのChromeでJSを実行するためにアプリ内プロキシを使用)よりも優れています。なぜなら、同期的なネイティブコールのデバッグをサポートし、一貫したランタイム環境を保証するからです。React DevTools、Metro、Inspectorなどとともに、Hermesデバッガは現在、ワンストップの開発者体験を提供するためにFlipperの一部となっています。
- React Nativeアプリの初期化パス中に割り当てられるオブジェクトは、多くの場合長寿命であり、世代別GCが利用する世代仮説に従いません。したがって、GCポーズのトリガーやTTIの遅延を避けるために、React NativeでHermesを設定して、最初の32MiBを直接古い世代に割り当てるようにしました(pre-tenuringとして知られています)。
- 新しいReact Nativeアーキテクチャは、JavaScriptエンジンをC++プログラムに埋め込むための軽量で汎用的なAPIであるJSI(JavaScript Interface)に大きく依存しています。JSエンジンを維持するチームがJSI APIの実装も維持することで、信頼性が高く、パフォーマンスに優れ、Facebookの規模で実証済みの最高の統合を提供できると確信しています。
- JavaScriptの並行処理プリミティブ(例:promise)とプラットフォームの並行処理プリミティブ(例:microtask)の両方を意味的に正しく、かつパフォーマンス良く実装することは、ReactのコンカレントレンダリングとReact Nativeアプリの将来にとって不可欠です。歴史的に、React Nativeのpromiseは、非標準の
setImmediate
APIを使用してポリフィルされていました。私たちは、JSエンジンからのネイティブなpromiseとmicrotaskをJSI経由で利用できるようにし、Web標準に最近追加されたqueueMicrotask
をプラットフォームに導入して、現代的な非同期JavaScriptコードをより良くサポートするよう取り組んでいます。
コミュニティ全体を巻き込む
HermesはFacebookにとって非常に素晴らしいものでした。しかし、私たちのコミュニティがエコシステム全体でHermesを使用して体験を動かすことができるようになるまで、私たちの仕事は終わりではありません。そうすることで、誰もがそのすべての機能を活用し、その完全な可能性を受け入れることができます。
新しいプラットフォームへの拡大
Hermesは当初、Android上のReact Native向けにのみオープンソース化されました。それ以来、コミュニティのメンバーがHermesのサポートをReact Nativeのエコシステムが拡大した他の多くのプラットフォームに広げているのを見て、私たちは非常に興奮しています。
Callstackは、React Native 0.64でHermesをiOSに導入する取り組みを主導しました。彼らはその達成方法について一連の記事を書き、ポッドキャストを配信しました。彼らのベンチマークによると、HermesはMattermostアプリでJSCと比較して、iOSでの起動時間を一貫して約40%改善し、メモリを約18%削減することができ、アプリサイズのオーバーヘッドはわずか2.4 MiBでした。ぜひご自身の目でライブでご覧になることをお勧めします。
MicrosoftはHermesをReact Native for WindowsとmacOSに導入しています。Microsoft Build 2020で、MicrosoftはHermesのメモリへの影響(ワーキングセット)がReact Native for Windows上でChakraエンジンよりも13%低いことを共有しました。最近、いくつかの合成ベンチマークで、彼らはHermes 0.8(Hadesと前述のSMIおよびポインタ圧縮最適化を搭載)が他のエンジンよりも30%〜40%少ないメモリを使用することを発見しました。驚くことではありませんが、React Native上に構築されたデスクトップ版Messengerのビデオ通話体験もHermesによって支えられています。
最後になりましたが、HermesはOculus Homeを含む、Oculus上でReactファミリーの技術で構築されたすべてのバーチャルリアリティ体験も支えています。
コミュニティをサポートする
私たちは、コミュニティの一部がHermesを採用するのを妨げているブロッカーがまだ存在することを認識しており、これらの欠けている機能のサポートを構築することにコミットしています。私たちの目標は、HermesがほとんどのReact Nativeアプリにとって正しい選択となるように、完全に機能を備えることです。以下は、コミュニティがHermesのロードマップをどのように形作ってきたかの例です。
Proxy
とReflect
は、Facebookが使用していないため、当初はHermesから除外されていました。また、Proxyを使用していない場合でも、Proxyを追加するとプロパティのルックアップパフォーマンスが低下する懸念もありました。しかし、ProxyはMobXやImmerなどの人気ライブラリのために、すぐに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機能を利用し、マッピングすることです。その代償として、プラットフォーム間で(しばしば軽微な)動作の差異が生じます。- MicrosoftはAndroidでのサポート構築に協力しました。それはECMA-402からES2020までのほぼすべてをカバーし、サイズへの影響はわずか3%(ABIあたり57-62K)です。Twitterで投票を行ったところ、結果は
Intl
をデフォルトで含めることに強く賛成でしたので、そのようにしました。これはリリースv0.8から利用可能です。 - FacebookはMajor League Hackingを後援し、リモートのオープンソースフェローシッププログラムを立ち上げました。昨年、私たちはHermesサンプリングプロファイラをリリースしました。今年、私たちのフェローはHermes、React Native、Callstackのメンバーと協力して、iOSでのHermes
Intl
のサポートを追加する予定です。ご期待ください!
- MicrosoftはAndroidでのサポート構築に協力しました。それはECMA-402からES2020までのほぼすべてをカバーし、サイズへの影響はわずか3%(ABIあたり57-62K)です。Twitterで投票を行ったところ、結果は
- コミュニティに影響を与える問題を発見するために、皆さんが私たちと協力してくださったことに感謝しています。
- ES2019で修正された
Array.prototype.sort
の安定性のような、重要な仕様の相違点を特定するのに協力していただきました。これは修正され、次のリリースで利用可能になります。 - デフォルトのヒープサイズ制限が小さすぎ、HermesのGC設定のカスタマイズに慣れていない多くのユーザーにとって不要なGCプレッシャーやOOMクラッシュを引き起こしていることがわかりました。そこで、デフォルトでほとんどのユーザーにとって十分すぎるように、512MiBから3GiBに増やしました。
- また、私たちの特化した
Function.prototype.toString
の実装が、不適切な機能検出を行うライブラリでパフォーマンスを低下させ、ユーザーがソースコードの注入を行うのを妨げていることも報告されました。これは、Hermesは可能な限り開発者の邪魔をせず、デファクトスタンダードを尊重すべきであるという私たちのスタンスを強化するのに役立ちました。
- ES2019で修正された
まとめ
要約すると、私たちのビジョンは、HermesをすべてのReact NativeプラットフォームでデフォルトのJavaScriptエンジンにする準備を整えることです。私たちはすでにその実現に向けて動き始めており、この方向性について皆さんからのご意見をお待ちしています。
エコシステムがスムーズに採用できるよう準備することは、私たちにとって非常に重要です。ぜひHermesを試してみて、フィードバック、質問、機能リクエスト、非互換性については、私たちのGitHubリポジトリにissueを立ててください。
謝辞
Hermesチーム、React Nativeチーム、そしてHermesを改善するために尽力してくれたReact Nativeコミュニティの多くの貢献者に感謝します。
また、個人的には(アルファベット順に)Eli White、Luna Wei、Neil Dhar、Tim Yung、Tzvetan Mikov、その他多くの方々に執筆中のご協力をいただいたことに感謝します。