モバイルアプリの起動時間最適化
起動が1秒縮むだけで離脱率は変わる。コールド/ウォーム/ホットスタートの違いとdyld・App Startupの内部動作から、効く施策を見極められる。
- 1.起動はコールド(プロセス新規生成)・ウォーム(プロセス生存)・ホット(Activity再生成のみ)の3種に分かれ、最適化対象はほぼ常にコールドスタート。
- 2.iOSはdyld共有キャッシュとページイン、Androidはクラスロードとバイトコード変換(AOT/JIT)が起動時間の下限を決める内部要因。
- 3.計測はTTID(初回フレーム表示)とTTFD(操作可能状態)を区別し、Time to First Frameだけを見ると体感速度を誤判定する。
なぜ「起動が速い」の定義から揃える必要があるのか
起動時間最適化は「アイコンタップから画面が出るまでを削る」という単純な話に見えて、実装対象を誤ると効果が出ません。原因は、OSが管理するプロセスの生存状態によって、そもそも実行される処理量がまったく異なるからです。プロセスを新規に立ち上げるのか、既存プロセスを使い回すのか、画面だけ作り直すのかで、最適化すべき層が変わります。まず起動を3種類に分解します。
コールド・ウォーム・ホットスタートの違い
| 種別 | プロセスの状態 | 主な処理 | 所要時間の目安 |
|---|---|---|---|
| コールドスタート | プロセスが存在しない。ゼロから生成 | プロセス生成、バイナリのロード、ランタイム初期化、アプリ初期化、初回画面構築 | 最も長い(数百ms〜秒) |
| ウォームスタート | プロセスは生存。画面や状態は破棄済み | ランタイム初期化は省略。アプリ初期化の一部と画面再構築のみ | 中程度 |
| ホットスタート | プロセスもActivity/ViewControllerも生存 | OSから前面へ戻すだけ。再初期化はほぼ不要 | 最短(数十ms以下) |
コールドスタートが重いのは、OSのプロセス管理から見て「何もない状態」から積み上げる必要があるからです。実行ファイルをディスクからロードし、動的リンクを解決し、ランタイム(iOSならObjective-C/Swiftランタイム、AndroidならART)を起動し、アプリ固有の初期化コードを走らせ、最初のUIを描画して初めて操作可能になります。ウォームスタートはこの積み上げの大部分(特にランタイム初期化と動的リンク)を省略できるため大幅に短縮され、ホットスタートはOSのバックグラウンド遷移をそのまま巻き戻すだけなので実質的に初期化を伴いません。
ウォーム・ホットはOSのプロセス管理次第で発生し、アプリ側の作り込みで短縮できる余地が小さい領域です。起動時間最適化の議論でボトルネックとして扱われるのは、実行すべき処理量そのものが最大になるコールドスタートです。以降もコールドスタートを前提に内部動作を見ていきます。
iOS: dyld共有キャッシュとバイナリ起動の内部
iOSのコールドスタートは、dyld(dynamic linker)がバイナリを実行可能にするまでの工程に大きく支配されます。アプリのMach-Oバイナリは通常、単体では動かず、UIKitやFoundationなど多数の共有ライブラリに依存します。この依存解決を都度ディスクから行うと遅いため、iOSは主要システムライブラリをあらかじめリンク・レイアウト済みの単一ファイルにまとめたdyld共有キャッシュをOSイメージに焼き込んでおき、起動時はこれをメモリにマップするだけで済ませます。
dyldが行う工程はおおよそ次の順序です。
1. 実行可能ファイル(Mach-O)をロード
2. 依存ライブラリを解決
- システムライブラリ → 共有キャッシュを mmap するだけ(高速)
- アプリ埋め込みのフレームワーク → 個別に mmap しシンボル解決(低速)
3. Rebase / Bind(ASLRオフセット適用、シンボルの実アドレス確定)
4. 各ライブラリの初期化子(+load、C++コンストラクタ等)を実行
5. main() 呼び出し
ここで実務上効くのが「埋め込みフレームワークの数を減らす」という施策です。共有キャッシュに載っているシステムライブラリはmmapのみで済みますが、アプリが独自に埋め込む動的フレームワーク(サードパーティSDKなど)は共有キャッシュの恩恵を受けず、個別にロードとシンボル解決が発生します。フレームワーク数が増えるほどdyldのbind処理が線形に近い形で伸びるため、静的リンクへの統合やフレームワークの削減が直接効きます。加えて、+loadメソッドやグローバルC++オブジェクトのコンストラクタはmain呼び出し前に強制実行されるため、ここに重い処理を書くとプロファイラ上で「main前」の時間として計上され続けます。
Xcodeの環境変数DYLD_PRINT_STATISTICSを有効にすると、dyldがmain呼び出し前にかけた時間の内訳(ライブラリロード、rebase/bind、初期化子実行)が出力されます。起動時間が長い場合、まずmain前とmain後のどちらが支配的かを切り分けることが最初の一手です。main前が重いなら埋め込みフレームワークの削減、main後が重いならアプリ初期化コードの見直しに進みます。
Android: App Startupとバイトコード最適化
Androidのコールドスタートは、Zygoteからのプロセスfork後、Applicationと最初のActivityが生成され初回フレームが描画されるまでの区間です。ここで効いてくる要因は大きく2つあります。
第一に、初期化処理の直列実行です。多くのライブラリは自前のContentProviderをマニフェストに登録して自動初期化を行いますが、ContentProviderのonCreate()はApplicationの生成直後、メインスレッド上で登録順にすべて同期実行されます。ライブラリが増えるほどこの直列処理が積み上がり、Application#onCreate()に到達する前に時間を消費します。Jetpack App Startupはこの問題への対処で、Initializerインタフェースに依存関係を明示させることで、個別ContentProviderの乱立を1つのContentProviderに集約し、かつ依存グラフに基づいてトポロジカルソートした順に初期化を実行します。ライブラリの数だけ生成・登録されていたContentProviderのインスタンス化コストを1つ分に抑えつつ、初期化順序を制御可能にする点が要です。
第二に、バイトコードの実行方式です。AndroidのアプリはDEXバイトコードとして配布され、これをネイティブコードへ変換する方式が起動時間を左右します。
| 方式 | 変換タイミング | 起動時間への影響 |
|---|---|---|
| インタプリタ実行 | 変換せずDEXを逐次解釈 | 初回実行は最速だが実行速度自体は最も遅い |
| JIT(実行時コンパイル) | 実行しながら頻出メソッドのみネイティブ化 | 起動直後はインタプリタ相当。徐々に高速化 |
| AOT(インストール時事前コンパイル) | インストール時やデバイスアイドル時に事前にネイティブ化 | 起動時は既にネイティブコード。初回起動は最速域 |
現行のART(Android Runtime)はJITとAOTを併用するハイブリッド方式で、インストール直後はインタプリタ/JIT主体で動き、デバイスがアイドルかつ充電中になったタイミングで使用頻度の高いメソッドをまとめてAOTコンパイルする、というプロファイルガイド最適化(Profile-Guided Compilation)を行います。つまり「同じアプリでもインストール直後の初回起動は遅く、数日運用後の起動は速くなる」のはART側の最適化が進行するためです。アプリ側でできる対策としては、起動経路のクラス数・メソッド数を減らす(不要な依存の削減、リフレクション多用の回避)ことが、インタプリタ/JIT段階の実行コストを直接下げます。
起動時間計測の指標
体感速度を正しく扱うには、「画面が出た」と「操作できる」を区別する指標が要ります。
| 指標 | 定義 | 計測される終端イベント |
|---|---|---|
| TTID(Time to Initial Display) | 起動要求から初回フレームが描画されるまで | 最初のレイアウトが画面に出た瞬間 |
| TTFD(Time to Full Display) | 起動要求からユーザーが実際に操作可能になるまで | 非同期データ取得やリスト描画完了などアプリが明示的に報告 |
TTIDはOSが自動計測でき、AndroidのlogcatにはDisplayed行として、iOSでも同種の初回描画タイミングが取得できます。しかしTTIDだけを最適化目標にすると、「枠だけ一瞬で出るがローディングスピナーが延々回る」画面でも指標上は高速と判定される落とし穴があります。AndroidのMacrobenchmarkライブラリが提供するreportFullyDrawn()はこの区別のためにあり、アプリが「本当に使える状態になった」タイミングを明示的にOSへ報告してTTFDとして計測させます。
「コールドスタートが最も重い理由」はプロセス生成+ランタイム初期化+動的リンクが総動員されるからだと説明できるようにします。iOSは共有キャッシュによりシステムライブラリのmmapは軽いが埋め込みフレームワークは重い、という非対称性が要点です。AndroidはApp Startupによる初期化の一本化と、ARTのAOT/JITハイブリッドによる「使うほど速くなる」特性が頻出論点です。計測ではTTIDとTTFDの違いを混同しないこと。
まとめ
起動時間最適化は、まずコールド・ウォーム・ホットのどれを縮めようとしているのかを見極めるところから始まります。実質的な最適化余地があるのはコールドスタートで、iOSではdyldによる動的リンク(特に埋め込みフレームワークのbindコスト)とmain前後の切り分けが、AndroidではApp Startupによる初期化の一本化とARTのAOT/JITプロファイルが、それぞれ起動時間の下限を決める内部要因です。そして施策の効果を正しく検証するには、初回描画のTTIDと操作可能になるTTFDを区別して計測することが欠かせません。プロセスやランタイムの生成コストそのものは/os/が扱うプロセス管理の話題と地続きで理解すると立体的になります。
モバイル開発 Article
モバイルアプリの起動時間最適化を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
モバイル開発
比較で見る軸
難易度: advanced / カテゴリ: モバイル開発 / タグ数: 5
導入後に効く点
iOSはdyld共有キャッシュとページイン、Androidはクラスロードとバイトコード変換(AOT/JIT)が起動時間の下限を決める内部要因。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- モバイル開発
- タグ数
- 5
判断チェックリスト
- 自社の用途が「モバイル開発 / パフォーマンス」に近いか確認する。
- 強みである「起動はコールド(プロセス新規生成)・ウォーム(プロセス生存)・ホット(Activity再生成のみ)の3種に分かれ、最適化対象はほぼ常にコールドスタート。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。