Cutting Android Cold Start From 9 Seconds to Under One
A practical walkthrough of the profiling and fixes that make the difference on the budget devices your users actually own.
Cold start is the first thing every user experiences and the last thing most teams measure. Worse, it is usually measured on the developer's flagship phone, where almost any amount of startup work looks acceptable.
On the entry-level hardware most of the world actually uses, the same app can take nine seconds to become usable.
Measure on the right device first
Before changing anything, get a real baseline:
adb shell am start -W -n com.yourapp/.MainActivity
The TotalTime value is your number. Run it five times on a cleared app and take the median.
Do this on the cheapest device in your supported range. If you do not own one, Firebase Test Lab rents them by the minute — which is significantly cheaper than shipping a slow app.
The usual culprits
In almost every audit we run, startup time is going to the same four places:
Everything initialising in Application.onCreate(). Analytics, crash reporting, image loaders, feature flags, dependency graphs — all constructed eagerly, all blocking the first frame. Most of them are not needed until well after the app is visible.
Synchronous disk and preference reads on the main thread. A SharedPreferences read looks harmless. Forty of them during startup is not.
A dependency graph built at launch. Dagger and Hilt can construct a surprising object tree before your first screen exists. Lazy injection fixes most of it.
A cold JIT. Without a baseline profile, the runtime interprets your startup path the first time it runs, every time the app is installed or updated.
Defer everything that can be deferred
The rule: if a library is not required to render the first frame, it does not initialise before the first frame.
App Startup makes this tidy, but the mechanism matters less than the discipline. Move non-essential initialisation behind a post-frame callback or a background dispatcher, and hold a Lazy reference where you can.
Ship a baseline profile
This is the highest-return change available, and it is largely mechanical. A baseline profile tells ART which code paths to compile ahead of time instead of interpreting on first run.
Add the androidx.baselineprofile plugin, write a small generator that exercises your startup path, and the runtime does the rest. Typical improvement is 20–30% on cold start for a few hours of work, with no change to your application code.
Trace before you guess
When the obvious fixes are exhausted, stop guessing and open a system trace. Perfetto shows exactly where the main thread is blocked — and it is frequently somewhere nobody suspected, like a content provider belonging to a third-party SDK.
Wrap your own suspicious sections so they appear in the trace:
trace("loadUserSession") {
sessionRepository.restore()
}
Protect the win
Performance regressions are not dramatic. They arrive one small initialisation at a time, and six months later you are back at nine seconds.
Add a startup benchmark to CI with Macrobenchmark and fail the build when the median regresses beyond a threshold. It is the only way the improvement survives contact with a roadmap.
Keep reading
Building Offline-First Android Apps That Actually Sync
Most apps treat connectivity as a given. Here is how we design data layers for the many places where it isn't.
Read articleInsightsWhat to Actually Look For in a Development Partner
Portfolio screenshots tell you very little. These are the questions that separate a studio that ships from one that bills.
Read article