Cold start vs warm start vs hot start — how do you actually reduce app startup time?
The three start types skip different amounts of work, so before optimizing anything you need to know which one you actually measured.
LEARN BY DOINGPut your intuition to work3 steps
Cold start vs warm start vs hot start — how do you actually reduce app startup time?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.The three start types are defined by exactly one thing: how much the system already has ready to go before your first frame appears. Getting this distinction precise is what separates a strong answer from "make it faster somehow."
A cold start happens when neither your process nor any of your Activity instances exist — after a device boot, after the user (or the system, under memory pressure) has killed your app, or on a genuine first launch. The system has to fork a new process from the Zygote, construct your Application object, initialize every ContentProvider declared in your manifest, and only then create, start, and resume the target Activity. This is the slow path, and it's the one most startup-optimization work targets.
A warm start means the process is already alive, but the specific Activity instance is not — most often because the system reclaimed it from memory while it sat in the background, so it has to go through onCreate() then onStart() then onResume() again, full view inflation and all, but it skips process creation, Application.onCreate(), and ContentProvider initialization entirely. It also covers a brand-new Activity being launched inside an already-running process.
A hot start means both the process and the target Activity instance are still alive in the background — the user pressed Home and came back, or navigated via recents. The system only needs onRestart() then onStart() then onResume(): no re-inflation, no process work, nothing to rebuild. This is the fastest path by a wide margin, and mostly your job here is not to accidentally turn it into a warm start by doing heavy, re-triggerable work in onResume().
Measuring this correctly matters as much as improving it. adb shell am start -W <package>/<activity> reports ThisTime, TotalTime, and WaitTime for a single launch, and Logcat prints a Displayed line the moment the first frame is drawn — that moment is Time To Initial Display, or TTID. TTID is not the same as "usable": a screen can draw its toolbar and an empty list well before the data it exists to show has arrived. Calling reportFullyDrawn() once the content the user actually came for is on screen marks Time To Full Display, or TTFD, and that's the number both Play Console's "slow start" vital and Macrobenchmark's StartupTimingMetric actually care about — optimizing TTID while ignoring TTFD is optimizing the number nobody experiences.
Reducing cold start is where most of the real work lives, because it's the slowest path and the one every new install goes through. Application.onCreate() is the first place to look: every SDK that initializes there runs, by default, synchronously on the main thread before your first Activity even starts. androidx.startup's Initializer interface replaces a pile of ad-hoc SDK-init calls with dependency-ordered initializers that also collapse what would otherwise be a separate ContentProvider per library — each with its own startup overhead — into a single one. Beyond that: ship a Baseline Profile, generated with the Macrobenchmark library's BaselineProfileRule, which tells ART's compiler which classes and methods are hot on a typical startup path so they're compiled ahead-of-time instead of interpreted cold on the very first run — this is one of the highest-leverage changes available for the amount of code it takes. Keep your dependency-injection graph lean, since an eagerly built Dagger/Hilt graph constructs every singleton binding at startup whether the first screen needs it or not — prefer lazy Provider bindings for anything that isn't on the critical path to the first frame. And audit specifically for main-thread I/O during startup — a synchronous preferences read or a disk-backed config load is exactly the kind of thing that's invisible in isolation and very visible in aggregate on a cold launch.
// BAD: every SDK blocks the very first frame, in registration order,// on the main thread, whether or not this screen needs any of them yet.class MyApp : Application() {override fun onCreate() {super.onCreate()AnalyticsSdk.init(this) // makes a network call under the hoodCrashReporter.init(this) // prepares a local disk bufferFeatureFlags.warmCache(this) // blocking read from diskImageLoader.init(this) // builds a thread pool + disk cache eagerly}}
// BETTER: only what must observe a crash from frame one stays inline;// everything else becomes an ordered, independently-testable Initializer.class MyApp : Application() {override fun onCreate() {super.onCreate()CrashReporter.init(this)}}class AnalyticsInitializer : Initializer<AnalyticsSdk> {override fun create(context: Context): AnalyticsSdk = AnalyticsSdk.init(context)override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()}class ImageLoaderInitializer : Initializer<ImageLoader> {override fun create(context: Context): ImageLoader = ImageLoader.init(context)override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()}
// Marking TTFD explicitly — the first frame (TTID) can be a toolbar and a// shimmer; reportFullyDrawn() marks the moment the real content lands.class HomeActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_home)viewModel.feed.observe(this) { items ->adapter.submitList(items)if (items.isNotEmpty()) reportFullyDrawn() // marks TTFD, not just TTID}}}
Warm and hot starts have a smaller surface area to optimize, but they're not automatically free. For warm start, letting the system destroy and recreate an Activity on a configuration change and rebuilding state through ViewModel plus saved-instance-state is generally the better engineering choice than fighting recreation with manually declared config-change handling — but only if that recreation path is itself cheap, which means the same startup discipline (lean onCreate(), no synchronous I/O) applies to it too. For hot start, the main risk is code in onResume() that isn't actually idempotent or cheap — re-querying data that hasn't changed, re-running layout work, re-subscribing listeners without checking whether they're already subscribed — because a hot start runs onResume() again with no process or dispatcher boundary to hide the cost behind.
The senior framing that ties this together: separate perceived latency from actual work. The Android 12+ SplashScreen API (with a backward-compatible library for older versions) shows a system-drawn splash the instant the process starts, before your code has run at all — it doesn't remove a single millisecond of real work, but it removes the blank window the user would otherwise stare at, which measurably changes how slow the launch feels. Done wrong — drawing your own themed splash Activity in addition to the system one — you get a visible double-splash flicker, a real anti-pattern that shows up in app-store reviews more than in profiler traces. And treat startup time as a metric gated in CI via Macrobenchmark, not something eyeballed occasionally, because regressions creep in one innocent Application.onCreate() addition at a time, and by the time someone notices "it feels slower," nobody remembers which of the last twenty pull requests did it.
🧠 Cold, warm, hot: check your understanding
0/4 · 0/4 answered1. Which best describes a warm start?
2. What does reportFullyDrawn() actually mark?
3. Why does a Baseline Profile speed up cold start?
4. What's the actual problem with adding a custom themed splash Activity on top of the Android 12+ system splash screen?