業務システムの社内アプリや会員向けアプリを React Native(JavaScriptでiOS/Androidアプリを開発できるフレームワーク)で運用していて、エンジン設定の変更を検討している方に向けた内容です。チュートリアル通りに設定を変えただけなのに、起動が遅くなった経験がある方の参考になれば幸いです。
具体的な事例を見てみます。React Native 0.73 を使うコンシューマー向けアプリで、android/app/build.gradle に enableHermes: true を追加した変更がマージされました。Hermes(Meta製のJavaScriptエンジンで、起動時にバイトコードを事前生成して実行を速くする仕組み)は本来、起動高速化のための機能です。
ところが Pixel 5(Android 11)での計測では、デフォルトの JSC(JavaScriptCore、iOS由来のJS実行エンジン)利用時は約1.2秒だった初期画面表示が、Hermes有効化後は約1.8秒に悪化しました。差分は0.6秒です。
何が起きたか、影響範囲はどこまでか
この0.6秒という数字は小さく見えますが、業務影響は三方向に広がります。
- ユーザー体験:スプラッシュ画面の遅延が「続ける」ボタンのタップ率を内部計測で3%押し下げた
- リリーススケジュール:原因調査とロールバック判断に1日を要し、スプリント目標が揺らいだ
- チーム運営:「なぜこの変更を入れたのか」を問う振り返りが発生し、会議時間を消費した
特に業務システムでは、この種の設定変更は「パフォーマンス改善のチュートリアルに従っただけ」という形で紛れ込みやすい点に注意が必要です。計測せずにマージされたことが、そもそもの発端でした。
なぜ起きるか、原因を段階的に分解する
Hermesが起動を遅くする理由は、単一ではなく複数の要因が重なって起きます。順に見ていきます。
1. エンジン本体の読み込みコスト
Hermesはネイティブライブラリ libhermes.so として配布され、最初のJavaScriptバイトコードを実行する前に、このライブラリ自体の読み込みと初期化が必要です。ディスクI/Oが遅い端末では、この読み込みだけでSnapdragon 765G環境で約120msのオーバーヘッドが発生します。JSCはOS側にあらかじめ組み込まれているため、この読み込みコストがほぼかかりません。
2. AOTコンパイルの一括読み込み
HermesはAOT(Ahead-Of-Time、実行前の事前コンパイル)方式でバイトコードをビルド時に生成します。これにより実行時のJITコンパイル(実行しながら最適化する処理)が不要になり、初回フレーム後のCPU使用率が30%減るケースもあります。
ただしAOTの結果は index.android.bundle という単一の巨大なバイトコードファイルにまとまります。このファイル全体をメモリにマッピングしてから実行する必要があり、低スペック端末では、JSCが行うスクリプトの部分読み込み(遅延ロード)よりも遅くなる場合があります。
3. サードパーティモジュールとの非互換
react-native-reanimated の2.12未満や react-native-gesture-handler の2.4未満など、一部のネイティブモジュールは eval や動的な import() に依存しています。Hermesはセキュリティとパフォーマンスの理由からデフォルトで eval を無効化しており、該当コードに遭遇すると次のような例外が発生します。
HermesException: Unexpected eval or new Function usage.この例外発生後、フォールバック処理として追加のバンドル解析が走ると、せっかくのバイトコードの優位性が打ち消され、起動時間がかえって伸びます。
4. デバッガ接続の待ち時間
Hermes有効時はChrome DevTools経由のリモートデバッグが無効化され、代わりにHermes Inspectorを使うデバッグ構成になります。コールドスタート時にこのWebSocketハンドシェイクを待つ構成が残っていると、約80msの遅延が加わります。本番ビルドでは本来スキップされるべき処理です。
5. ビルド時間の増加
Hermes有効化は hermesc によるバイトコード生成ステップをGradleパイプラインに追加します。CPUリソースが限られたCIランナーでは、Androidビルド時間が約30%(8分→10分)増えるケースが報告されています。ビルドが長くなるとリリース頻度が下がり、依存関係の更新漏れが本番に混入するリスクも高まります。
自分のプロジェクトが該当するか確認する方法
対策に入る前に、まず自分の環境がこの問題の対象かどうかを切り分けます。
設定ファイルの確認: android/app/build.gradle を開き、enableHermes の値を確認します。React Native 0.70以降では android/gradle.properties に hermesEnabled=true という形で設定されている場合もあります。
バージョンの確認: package.json の react-native のバージョンと、react-native-reanimated や react-native-gesture-handler のバージョンを確認します。前述の非互換バージョン未満であれば、該当の可能性が高まります。
実機での計測: 疑わしい端末がある場合、実際に起動時間を測定します。
cd android
./gradlew clean assembleRelease
adb install -r app/build/outputs/apk/release/app-release.apk
adb logcat -s ReactNativeJS:V | grep -i "JS load time"このログで表示される読み込み時間が、Hermes有効/無効の両方のビルドでどれだけ差があるかを比較します。業務システムであれば、社内配布端末のうち最も古いスペックの機種で計測するのが現実的です。
対策の手順
切り分けができたら、次の手順で対応を進めます。
1. 同一コミットでの比較ビルドを作成します。enableHermes: true を追加した変更の直前のコミットと、追加後のコミットの両方で、同じ実機を使って起動時間を記録します。
2. 依存モジュールのバージョンを棚卸しします。eval や動的 import() に依存するモジュールがないか、npm ls react-native-reanimated react-native-gesture-handler などで確認し、必要に応じてアップデートします。
3. 本番ビルドのデバッグ構成を見直します。Hermes Inspectorの接続待ちが本番ビルドに残っていないか、android/app/build.gradle のリリースビルド設定を確認します。
4. 低スペック端末でのベンチマークをCIに組み込みます。ミドルレンジ端末相当のエミュレータまたは実機を使った起動時間計測を、リリース判定のチェック項目に加えます。
5. 段階的ロールアウトを検討します。全ユーザーに一括適用せず、一部ユーザー向けビルドでHermesの効果を検証してから全体展開する判断もできます。
業務システムの場合、ユーザー端末のスペック分布が社内アプリと一般消費者向けアプリで大きく異なります。古い支給端末が多い現場では、特に実機計測を省略しないことが欠かせません。
まとめ
Hermes導入による起動時間の悪化は、エンジン読み込み・AOTバンドルの一括マッピング・非互換モジュール・デバッガ接続・ビルド時間という5つの要因が組み合わさって起きます。
対応の起点は、設定ファイル(build.gradleやgradle.properties)の確認と、依存モジュールのバージョン棚卸しです。そのうえで実機ベンチマークを使い、Hermes有効/無効の両方を数値で比較してから判断するのが現実的な進め方になります。
チュートリアル記事の設定をそのままマージする前に、手元の最低スペック端末で起動時間を一度測ってみることをおすすめします。