「ロースペックなAndroid端末でアプリの起動に5秒以上かかる」。これは、月間アクティブユーザー(MAU)が10万人を超えた頃、私たちのチームが直面した最大の課題でした。iOSでは快適に動作していても、AndroidのエントリーモデルではJavaScriptの解析とコンパイルのオーバーヘッドが致命的なボトルネックになります。ユーザーは白い画面(White Screen of Death)を3秒以上見続けると、アプリをそっと閉じます。本稿では、レガシーなJavaScriptCore(JSC)からHermes Engineへ移行し、さらにProGuardによる難読化と不要コード削除を徹底することで、React Nativeパフォーマンスを劇的に改善した全プロセスを共有します。単なる設定変更ではなく、本番環境で発生したクラッシュのトラブルシューティングも含めた「泥臭い」最適化の記録です。
JSCの限界とHermes Engineの優位性分析
従来のReact Nativeアーキテクチャでは、JavaScriptCore(JSC)がデフォルトのエンジンとして使用されていました。JSCは優秀ですが、モバイル開発において一つの大きな弱点を抱えています。それは「JIT(Just-In-Time)コンパイル」への依存です。アプリ起動時にJSバンドルを読み込み、パースし、コンパイルする一連の処理がメインスレッドを占有するため、TTI(Time to Interactive)が遅延します。
今回のプロジェクト環境は以下の通りです。
- Framework: React Native 0.72.6
- Target OS: Android 10+ (Min SDK 24)
- Traffic: 1日あたり約15,000セッション
- 課題: APKサイズが45MBを超肥大化し、Android Vitalsでの「Bad Cold Start」率が2.5%を超過。
失敗談:安易なProGuard適用によるクラッシュ
Hermes導入の前に、「まずはバンドルサイズ削減だ」と息巻いて、android/app/build.gradleでenableProguardInReleaseBuilds = trueを設定しました。理論上はこれで未使用コードが削除されるはずです。しかし、ビルドして実機で起動した瞬間、アプリはクラッシュしました。
java.lang.ClassNotFoundException: com.thirdparty.library.SomeClass
原因は、リフレクションを使用しているサードパーティ製ライブラリ(特に古いAnalytics系SDKや特定のUIライブラリ)が、ProGuardの強力な名前変更(Obfuscation)によって参照先を見失ったことでした。アプリ最適化において、各ライブラリのproguard-rules.pro設定を無視して「とりあえず有効化」するのは自殺行為です。この失敗から、ライブラリごとの依存関係を洗い出し、ホワイトリストを作成する必要性を痛感しました。
ソリューション:Hermes有効化とProGuardの適切な構成
最終的にたどり着いた安定構成は、「Hermesによるバイトコード化」と「ProGuardによる慎重な不要コード削除」のハイブリッド戦略です。
まず、android/app/build.gradleを編集し、Hermesを有効化します。React Native 0.70以降ではデフォルトになりつつありますが、明示的な設定とメモリ調整が重要です。
// android/app/build.gradle
project.ext.react = [
enableHermes: true, // Hermes Engineを有効化
]
def enableProguardInReleaseBuilds = true // バンドルサイズ削減のため必須
def enableShrinkResourcesInReleaseBuilds = true // 画像などのリソース圧縮
android {
defaultConfig {
// ...
}
buildTypes {
release {
minifyEnabled enableProguardInReleaseBuilds
shrinkResources enableShrinkResourcesInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
// 重要: Hermes使用時のデバッグシンボルを残す設定(Crashlytics用)
ndk {
debugSymbolLevel 'SYMBOL_TABLE'
}
}
}
}
次に、先ほどのクラッシュを防ぐためにandroid/app/proguard-rules.proを調整します。特にReact NativeやHermes自体に必要なルール、およびリフレクションを使用する主要ライブラリを保護します。
# React Nativeの基本ルール
-keep class com.facebook.react.** { *; }
-keep class com.facebook.hermes.reactexecutor.** { *; }
# OkHttp3などのネットワークライブラリ
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
# アニメーションライブラリ(Reanimatedなど)がJSIを使用する場合の保護
-keep class com.swmansion.reanimated.** { *; }
-dontwarn com.swmansion.reanimated.**
# その他、リフレクションを使用するモデルクラス等
# -keep class com.myapp.models.** { *; }
ProGuardの設定は「引き算」ではなく「足し算」の思考が必要です。基本設定から始めて、クラッシュログを見ながら必要なクラスを-keepで追加していくのが、遠回りのようで確実な近道です。
ベンチマーク結果と分析
以下の表は、Samsung Galaxy A10(低スペック機)における最適化前後の比較データです。Hermes導入とProGuard設定のみを変更し、コードロジックには一切手を加えていません。
| 指標 | JSC (Before) | Hermes + ProGuard (After) | 改善率 |
|---|---|---|---|
| APKサイズ | 45.2 MB | 28.4 MB | -37% |
| Cold Start (TTI) | 4.8 秒 | 1.9 秒 | 60% 高速化 |
| メモリ使用量 (起動直後) | 180 MB | 115 MB | -36% |
この劇的な改善の主因は、Hermesのバイトコードがメモリマップ(mmap)可能である点にあります。JSCでは全JSテキストをヒープに読み込む必要がありましたが、Hermesでは必要な部分だけをメモリにページングできます。これにより、低メモリ端末でのOSによるプロセス強制終了(OOM Killer)のリスクも低減しました。
Hermes公式リポジトリを確認する導入時の注意点とエッジケース
すべてがバラ色ではありません。Hermesへの移行にはいくつかの「副作用」が存在します。これらを知らずに進めると、リリース直前に慌てることになります。
chrome://inspect)を使用する必要があります。
また、正規表現(RegExp)の実装において、JSCとHermesで微妙な挙動の違いがあります。特に「Lookbehind(後読み)」などの高度な正規表現機能は、古いバージョンのHermesではサポートされていませんでした(最新版では改善されていますが、要検証です)。iOSに関しては、SafariのJavaScriptCoreがOSレベルで高度に最適化されているため、Androidほどの劇的なサイズ削減や速度向上は体感できない場合がありますが、バイトコード統一の観点から両OSでの採用を推奨します。
結論
React Nativeパフォーマンスの最適化において、Hermesエンジンの導入はもはや選択肢ではなく「必須」の標準構成と言えます。ProGuardと組み合わせることで、バンドルサイズ削減と起動速度の向上を同時に達成でき、ユーザー体験を底上げします。もし、まだJSCを使用しているのであれば、次回のスプリントでHermesへの移行を検討してみてください。初期設定のトラブルシューティングにかかるコストを補って余りあるパフォーマンスが得られるはずです。
Post a Comment