Skip to content
Umar ManzoorBook a call
← Writing

Why your app's cold start is quietly costing you users

July 15, 2026

A five-second cold start doesn't annoy your users. It quietly kills your retention.

The people you lose to a slow launch never complain. They don't leave a one-star review. They just don't come back — and three months later it surfaces as a "retention problem" no one can trace to a cause. This is the full version of that story: what cold start actually is, how to measure it properly, where the time hides, and the specific things to cut.

Cold, warm, hot — and why only one matters here

Three launch states, and they are not the same problem:

  • Cold start — the process isn't running. The OS has to create it, spin up the engine/runtime, run your init, and render the first frame. This is the slowest and the one that greets new and returning users after a while away.
  • Warm start — the process was killed but some things are cached; faster.
  • Hot start — the app is resumed from memory; near-instant.

Optimise cold start. It's the worst case, it's what a first-time user feels, and it's the number that correlates with whether they come back.

Measure it before you touch anything

Almost every team optimises by guessing. Don't. Get a number first, on a real mid-range device, not a flagship on office Wi-Fi.

Android

  • Quick signal: adb shell am start -W <package>/<activity> reports TotalTime and WaitTime per launch.
  • Real rigour: a Macrobenchmark with StartupTimingMetric gives you Time-to-Initial-Display and Time-to-Full-Display across runs.
  • Field data: Play Console → Android vitals shows cold-start times from real users, segmented by device — that's the distribution that actually matters.

Flutter

  • flutter run --trace-startup --profile writes start_up_info.json with timeToFirstFrameMicros and time-to-first-frame-rasterized.
  • DevTools → Performance timeline to see what's blocking the first frame.
  • Instrument the gap from main() to your first meaningful frame yourself and log it.

Both

  • Firebase Performance captures an _app_start trace from real users automatically — the cheapest way to watch the field number over time.

Pick one, take a baseline, and write it down. You can't defend an improvement you never measured.

Where the time actually goes

When you profile that first window, the culprits are almost always the same handful:

  • Eager SDK / plugin initialisation — analytics, crash reporting, ads, remote config, and half a dozen third-party SDKs all initialising synchronously before first frame.
  • A dependency-injection graph built up front — constructing objects the first screen never touches.
  • Synchronous work on the main thread — JSON deserialization, disk reads, decryption, migrations running where they block rendering.
  • A blocking network call at launch — waiting on config or auth before showing anything.
  • Heavy first-frame build — an over-complex initial widget tree or layout pass.
  • App size and cold code paths — more code to load and JIT/AOT resolve; oversized assets and fonts loaded eagerly.

The fixes, in rough order of payoff

  1. Defer non-critical initialisation. Anything not needed to render the first screen runs after first interaction, not before it. Most analytics, ad, and remote-config init can wait a few hundred milliseconds.
  2. Make dependency injection lazy. Construct on first use, not at boot. GetIt's registerLazySingleton, Hilt's lazy bindings — the first screen should build only what it needs.
  3. Get work off the main thread. Move deserialization, decryption, and migrations to a background isolate (Flutter) or a worker/coroutine (Android) so the first frame isn't blocked.
  4. Kill the blocking launch network call. Render from cached config immediately, then refresh in the background. A launch should never wait on a round trip.
  5. Cache what you recompute every launch. Config, feature flags, parsed data — read from disk once, not rebuilt from scratch each start.
  6. Shrink the first frame. Simplify the initial widget tree/layout; defer heavy sections until after paint.
  7. Trim app size. Enable R8/shrinking, split by ABI/deferred components, and stop eagerly loading fonts and assets the first screen doesn't show. Smaller binaries cold-start faster, especially on low-end Android.

None of these are features. They're all "stop making the user wait for work that could happen later."

Then make it a budget, not a one-off

The reason cold start silently regresses is that no one owns the number. Fix that:

  • Add a startup trace you track release over release (Firebase Performance or your own metric).
  • Put a budget on time-to-interactive and fail CI or flag the release when a change blows it.
  • Watch the field distribution, not your dev device — your users are on hardware you don't own.

Startup then becomes something the team can see and defend, instead of a number that quietly drifts until it shows up in the retention chart.

A 20-minute audit you can run this week

  1. Measure cold start on a real mid-range device (adb shell am start -W or --trace-startup).
  2. List everything that runs before your first screen is interactive.
  3. For each item ask: does the first screen actually need this? Usually two or three don't.
  4. Defer those; move any blocking work off the main thread.
  5. Re-measure. Bank the win. Add the metric to your dashboard so it stays won.

Most teams don't know their app's cold-start time. That's the first thing to fix — because everything above is invisible until you have the number.


This is the kind of work I do as a consultant — mobile performance audits and Flutter/Android optimisation for teams whose app has quietly gotten slower. If that's you, book a call.