跪拜 Guibai
← All articles
Android · Android Studio · Performance Optimization

From Icon Tap to First Frame: Measuring and Cutting Android Cold Start by 70%

By 碳基Agent ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Cold start is a user-visible cost on every launch, and the techniques here apply to any Android codebase — hidden ContentProviders and serialized SDK init can silently eat over half of launch time on mid-range hardware, so measuring before optimizing isn't optional.

Summary

A concrete Android cold-start optimization walkthrough demonstrates that the biggest launch-time costs often hide in places developers never touch directly. On a Xiaomi Mi 6 (Snapdragon 835), the baseline cold start took 1269.5ms; ContentProvider initialization plus Application.onCreate consumed 58.3% of that before the UI even started measuring layout. On a flagship Xiaomi 13 Pro, the same code took 760.5ms, showing why testing only on flagship hardware produces false confidence.

The article applies four targeted cuts: tier Application.onCreate initialization into P0/P1/P2 (crash reporter stays synchronous, push/analytics go to background coroutines, map SDK becomes lazy); strip hidden ContentProviders from third-party SDKs and conditionalize Jetpack App Startup components like WorkManager; move first-Activity I/O and JSON parsing to Dispatchers.IO with a skeleton first frame; and apply Baseline Profile with Startup Profile as the final step. Each cut is verified with Macrobenchmark on two real devices, and the trade-off between TTID and TTFD becomes visible: on the low-core Mi 6, background threads can compete with the main thread and actually slow down full display.

Final results show TTID dropping to 380.5ms on the Mi 6 (-70%) and 238.0ms on the Mi 13 Pro (-68.7%), with Baseline Profile alone cutting first-draw time by 41.8% on the older chip. The article also provides a reusable toolchain: `am start -W`, `Displayed` logs, Perfetto with custom trace sections, and Macrobenchmark for multi-run medians instead of single-shot guesses.

Takeaways
— On a Xiaomi Mi 6 (Snapdragon 835), baseline cold-start TTID was 1269.5ms; on a Xiaomi 13 Pro (Snapdragon 8 Gen 2) it was 760.5ms.
— ContentProvider initialization plus Application.onCreate accounted for 58.3% of launch time on the Mi 6 (224.0ms + 515.6ms), while the first frame draw was only 14.1%.
— Step 1 tiering of Application.onCreate reduced its main-thread time from 515.6ms to 48.3ms on the Mi 6 and from 192.5ms to 21.8ms on the 13 Pro.
— Removing third-party ContentProviders and conditionalizing WorkManager dropped the ContentProvider stage from 223.4ms to 2.2ms on the Mi 6 (-99%).
— Moving first-Activity JSON parsing to Dispatchers.IO with a skeleton screen cut Activity.onCreate from 172.2ms to 13.1ms on the Mi 6, but on that low-core device TTFD only improved by 79.5ms because background threads competed with the main thread.
— Baseline Profile precompiled Compose hot paths: on the Mi 6, first draw fell from 193.1ms to 112.4ms (-41.8%), and TTID dropped from 490.5ms to 380.5ms.
— Final optimized medians: 380.5ms TTID on Mi 6 (-70%) and 238.0ms on Mi 13 Pro (-68.7%), from starting points of 1269.5ms and 760.5ms.
— A three-layer diagnostic chain is required: `am start -W` and `Displayed` logs for smoke tests, Perfetto with `Trace.beginSection` for per-stage breakdown, and Macrobenchmark with 10 cold-start medians for statistically valid acceptance.
— Jetpack App Startup consolidates providers but still runs `Initializer.create()` synchronously on the main thread; WorkManager can be removed from the startup path and initialized on demand via `Configuration.Provider`.
— Baseline Profile helps first launch only if installed via Google Play or ProfileInstaller; `CompilationMode.Partial(BaselineProfileMode.Require)` in Macrobenchmark forces real profile installation to avoid false positives.
Conclusions

The article's measurements expose a common trap: flagship-only testing hides a 2.7x amplification of synchronous init cost on older chips, so release-time complaints from mid-range users almost always stem from code that felt fine in the developer's hands.

Moving work off the main thread is not a free lunch: on a low-core device, background init threads can steal CPU from the first frame's Compose render, widening the TTID-to-TTFD gap even as TTID improves — a real-world reminder that scheduling priorities matter more than raw offloading.

The prevalence of hidden ContentProviders in third-party SDKs, registered via manifest merger, is a silent design anti-pattern: it lets libraries initialize without consent or knowledge, and the only safe response is a hardened merged-manifest review process before every release.

Baseline Profile's return on investment is highly device-dependent: it yields a 41.8% first-draw improvement on a 2017 chip but only marginal median gains on a flagship, while still stably trimming the tail-latency distribution — worth measuring per target device class.

Concepts & terms
Cold start
Launching an app from scratch — the system has no existing process — so Zygote forks a new process, ART VM initializes, Application is created, then the first Activity. This path is the most expensive to optimize.
TTID (Time To Initial Display)
The moment the system draws and submits the first frame of an app. It's the default metric in Android Vitals, but it only measures the first visible frame — which can be a pure white screen or skeleton.
TTFD (Time To Full Display)
The moment when an app's core content is rendered and the user can actually interact. Developers must call `reportFullyDrawn()` (or `ReportDrawnWhen` in Compose) to report it; without that, Macrobenchmark won't output this metric.
Baseline Profile
A list of hot classes and methods packaged inside the APK (`assets/dexopt/baseline.prof`). During install (or via ProfileInstaller) the system AOT-compiles those methods into machine code so the first launch skips interpreted or JIT execution, speeding up CPU-bound code paths.
Startup Profile / DEX layout optimization
An AGP 8.2+ feature that instructs R8 to place startup-critical classes at the beginning of the main `classes.dex`, reducing disk page faults during cold-start class loading.
ContentProvider auto-initialization
Libraries can register a ContentProvider in their own manifest; after merge, the system instantiates it and calls `onCreate()` before `Application.onCreate()` on the main thread — effectively hiding an arbitrary initialization step from the app developer.
Jetpack App Startup
A library that consolidates multiple libraries' ContentProviders into a single `InitializationProvider`, reducing system overhead. It does not make initialization asynchronous — all `Initializer.create()` calls still run synchronously on the main thread.
Macrobenchmark
A Jetpack benchmark API that repeatedly kills and cold-starts an app, measuring TTID/TTFD across 10+ runs and reporting min, median, and max. It supports `CompilationMode` to simulate first install (None) vs. profile-installed (Partial).
DAG startup framework
A task scheduler that models SDK initializations as nodes in a directed acyclic graph, resolving dependencies and running independent tasks concurrently across threads, typically used when startup tasks grow to dozens and hand-rolled coroutines become unwieldy.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗