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

Hermesをデフォルトにするために

·所要時間 12分
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ほど複雑なアプリでは、平均一時停止時間は200ミリ秒、p99では1.4秒でした。Facebook for Androidのユーザーベースは大きく多様であるため、7秒にも及ぶのを見たことがあります。

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

パフォーマンスのペインポイントへの取り組み

アプリケーションの起動時間は多くのアプリの成功にとって重要であり、私たちは継続的にReact Nativeの限界を押し広げています。Hermesに実装する新しいJavaScript機能については、本番環境のパフォーマンスへの影響を注意深く監視し、メトリックが低下しないようにしています。Facebookでは現在、MetroのHermes専用のBabel変換プロファイルを実験的に使用して、12個のBabel変換をHermesのネイティブESNext実装に置き換えています。多くのサーフェスで**TTIが18〜25%向上**し、**バイトコードサイズが全体的に減少**することが確認されており、OSSでも同様の結果が得られると期待しています。

起動パフォーマンスに加えて、React Nativeアプリ、特にバーチャルリアリティでは、メモリフットプリントが改善の機会として認識されました。JavaScriptエンジンとして低レベルの制御ができるおかげで、ビットとバイトを絞り出すことで、メモリ最適化のラウンドを提供することができました。

  1. 以前は、64ビットアーキテクチャ上で浮動小数点の倍精度とポインタを表すために、すべてのJavaScript値は64ビットのNaN boxingでエンコードされたタグ付き値として表現されていました。しかし、ほとんどの数は小さな整数(SMI)であり、クライアントサイドアプリケーションのJavaScriptヒープは一般的に4GiBを超えないと予想されるため、実際にはこれは無駄が多いです。これを解決するために、SMIとポインタを29ビットでエンコードする新しい32ビットエンコーディングを導入しました(ポインタは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デバッガーを使用したデバイス上でのJavaScriptデバッグを、Chrome DevTools Protocolを使用することでサポートしています。これは、従来の「リモートJSデバッグ」(デスクトップChromeでJSを実行するためにアプリ内プロキシを使用する)よりも優れています。同期ネイティブ呼び出しのデバッグをサポートし、一貫したランタイム環境を保証するためです。React DevTools、Metro、Inspectorなどと並んで、Hermesデバッガーは現在、Flipperの一部となり、ワンストップの開発者エクスペリエンスを提供しています。
  • React Nativeアプリの初期化パス中に割り当てられたオブジェクトは、多くの場合、寿命が長く、世代別GCで活用される*世代仮説*に従いません。そのため、React NativeのHermesを設定して、最初の32MiBを古い世代(*プリテニュアリング*として知られる)に直接割り当て、GCの一時停止とTTIの遅延を回避しています。
  • 新しいReact Nativeアーキテクチャは、JSI(またはJavaScriptインターフェース)に大きく基づいています。これは、JavaScriptエンジンをC ++プログラムに埋め込むための軽量で汎用的なAPIです。JSエンジンを保守するチームがJSI API実装も保守することで、Facebookの規模で信頼性が高く、パフォーマンスが高く、実証済みの最高の統合を提供できると確信しています。
  • JavaScriptの並行処理プリミティブ(例:Promise)とプラットフォームの並行処理プリミティブ(例:マイクロタスク)の両方を意味的に正しくかつ高パフォーマンスにすることは、Reactの並行レンダリングとReact Nativeアプリの将来にとって重要です。これまで、React NativeのPromiseは、標準化されていないsetImmediate APIを使用してポリフィルされていました。JSエンジンからのネイティブPromiseとマイクロタスクをJSI経由で利用できるようにし、最新の非同期JavaScriptコードをより適切にサポートするために、Web標準に最近追加されたqueueMicrotaskをプラットフォームに導入する作業を行っています。

コミュニティ全体を巻き込む

HermesはFacebookで非常に役立っています。しかし、コミュニティがエコシステム全体のエクスペリエンスを強化するためにHermesを使用し、すべての機能を活用し、その潜在能力を最大限に発揮できるようになるまで、私たちの仕事は完了しません。

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

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

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

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

最後に、HermesはOculus Homeを含む、Oculus上のReactファミリーのテクノロジーで構築されたすべての仮想現実エクスペリエンスも強化しています。

コミュニティのサポート

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

  • ProxyReflectは、Facebookでは使用されていないため、当初Hermesから除外されていました。また、Proxyを追加すると、Proxyが使用されていない場合でもプロパティルックアップのパフォーマンスが低下するのではないかと懸念していました。しかし、Proxyは、MobXImmerなどの人気のライブラリのために、すぐに最も要望の多かった機能になりました。慎重に評価した結果、コミュニティのためだけに構築することを決定し、非常に低コストで実装することができました。これは私たちが使用していない機能であるため、コミュニティに頼ってその安定性を証明しました。最初にフラグの背後でProxyをテストし、リリースv0.4v0.5のオプトインnpmパッケージを作成しました。そして、v0.7以降、デフォルトで有効になっています。
  • ECMAScript国際化API仕様(ECMA-402、またはIntlは、2番目に要望の多かった機能でした。Intlは膨大なAPIセットであり、実装には多くの場合、6MB相当のUnicode CLDRデータを含める必要があります。そのため、FormatJS(別名react-intlのようなポリフィルや、コミュニティJSCの国際バリアントビルドのようなJSエンジンは非常に巨大です。Hermesのバイナリサイズを大幅に増やすことを避けるため、プラットフォーム間での動作のばらつき(多くの場合軽微)を犠牲にして、オペレーティングシステムに含まれるライブラリによって提供されるICU機能を利用およびマッピングすることにより、別の戦略で実装することを決定しました。
    • MicrosoftはAndroidでのサポートの構築に協力しました。ES2020までのECMA-402のほぼすべてをカバーしており、サイズへの影響はわずか3%(ABIあたり57〜62K)です。Twitterで投票を実施したところ、デフォルトでIntlを含めることに非常に賛成の意見が多かったため、それを実行し、リリースv0.8から利用できるようになりました。
    • Facebookは、Major League Hackingのスポンサーとなり、リモートオープンソースフェローシッププログラムを立ち上げました。昨年、私たちはHermesサンプリングプロファイラをリリースしました。今年は、Hermes、React Native、Callstackのメンバーと協力して、iOSでHermes Intlのサポートを追加します。ご期待ください!
  • コミュニティに影響を与える問題の発見にご協力いただいた皆様に感謝いたします。

まとめ

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

エコシステムがスムーズに移行できるように準備することは非常に重要です。Hermesをお試しいただき、フィードバック、質問、機能リクエスト、非互換性については、GitHubリポジトリに問題を報告してください。

謝辞

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

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