A user taps your icon and waits. Whatever happens in the next second or three is their first impression, and a slow launch is the kind of friction that gets an app deleted before it is ever really used. App cold start time is both highly visible and, for most apps, badly optimised — largely because the work that slows it down is invisible in day-to-day development, when the app is already warm in memory. Understanding what actually happens during launch is the whole battle.
Cold, warm, and hot: not the same launch
First, three different launches that get lumped together. A hot start brings an already-running app back to the foreground — near instant, nothing to do. A warm start recreates some state for an app still partly in memory. A cold start is the expensive one: the app is not in memory at all, so the operating system must create the process from scratch, initialise the runtime, and build the first screen from nothing.
Cold start is what you must optimise, and it is exactly the one you rarely experience while developing, because your app is almost always already warm. This is why launch performance quietly rots: the people building the app never feel the cold start their users feel every morning. The first step is simply to measure the right thing — force-stop the app and launch it fresh, every time you test startup.
Everything important happens on one thread
The reason launch is slow is almost always the same: too much work crammed onto the main thread before the first frame can render. Both platforms build their UI on a single main (UI) thread, and until that thread is free to draw, the user stares at a blank screen or a static launch image. Every initialisation you run synchronously during startup is time the main thread cannot spend drawing.
This is the core insight that makes launch optimisation tractable: it is not that your app does too much, it is that it does too much before showing anything, on the one thread that draws the screen. The fix is almost always the same shape — get work off the startup path, off the main thread, or defer it until after the first frame.
The usual culprits
Launch bottlenecks cluster into a few recognisable patterns, and knowing them shortcuts the investigation.
Eager SDK and library initialisation is the most common. Analytics, crash reporting, ad networks, feature-flag clients — each wants to initialise at startup, and each adds milliseconds to the main thread before the first frame. A dozen third-party SDKs all initialising synchronously in your application’s startup can add a second or more on their own, and most of them do not need to be ready before the first screen appears. On Android, the App Startup library exists precisely to sequence and defer these; the goal is to initialise on a background thread or lazily on first use, not eagerly on the critical path.
Synchronous I/O on the main thread is the next: reading a large preferences file, opening and migrating a database, loading a config from disk — all blocking the main thread before the UI can draw. Any disk or network work during startup should be asynchronous, off the critical path.
Doing real work in the wrong place — heavy logic in an Android Application.onCreate or an iOS application(_:didFinishLaunching:), both of which run before anything is on screen — is the third. These entry points should do the minimum required to show the first frame and nothing more.
Show something instantly, finish loading after
The most effective structural change is to decouple “the app is on screen” from “the app is fully loaded.” You do not need all your data ready to show a usable first frame — you need enough to draw something, then you can fill in the rest.
Render the first screen with a lightweight placeholder or skeleton immediately, then load the real content once the UI is up. This is the mobile version of the same idea that makes the web feel fast — acknowledge the user instantly, do the heavy work after the first paint, exactly as in our critical rendering path teardown. The user perceives the app as fast because they saw a response immediately, even though the full content arrived a moment later. Perceived performance is what they judge, and it is often improvable even when raw load time is not.
A related trap worth naming: a custom animated splash screen that runs for a fixed duration is not hiding your slow launch, it is your slow launch, deliberately extended. Use the platform’s own fast splash mechanism and get to interactive as quickly as possible, rather than covering slowness with animation the user still has to sit through.
Measure it honestly
You cannot optimise launch by feel, and both platforms give you real numbers. Android reports “time to initial display” and “time to full display” in logcat and in the Play Console’s vitals, which aggregate real launches from real devices — the data that reflects your actual users rather than your fast development phone. iOS exposes launch metrics through Instruments and the Xcode Organizer’s launch reports, drawn from real field usage.
The two rules that keep the measurement honest are the same as for battery. Always test a genuine cold start — force-stop first — because a warm start hides the very work you are hunting. And weight field data over your own device, because your recent flagship on a fast connection is nobody’s worst case, and launch time is exactly the metric where the slowest devices matter most. This is the same discipline as diagnosing drain in our battery guide — the tools differ, but the honesty required is identical.
Precompilation: paying the startup cost before the user does
A significant chunk of cold-start time on both platforms goes into work the runtime does to get your code ready to execute — and a family of ahead-of-time techniques moves that work out of the launch and into the build or install, so the user never pays for it.
On Android, code is normally compiled just-in-time as it runs, which means the first launch does extra work interpreting and compiling the very code paths involved in starting up — the paths the user is waiting on. Baseline Profiles let you ship a list of the classes and methods exercised during startup and critical user journeys, and the system precompiles exactly those ahead of time. The startup path is native from the first launch instead of being compiled while the user waits, and the reported improvements are substantial for a technique that requires no change to your actual code — just a profile generated from a test that exercises the launch. The details are in our baseline profiles guide, and it is close to free startup performance for the effort.
The principle generalises beyond Android: the fastest startup work is the work you already did. Anything you can compute at build time rather than launch time — a precompiled profile, a prebuilt index, a bundled initial dataset rather than one fetched or migrated on first run — is time removed from the critical path entirely. A database that ships pre-seeded starts faster than one that builds itself on first launch; configuration resolved at build time costs nothing at startup.
The mindset that ties it together is to interrogate every piece of startup work with one question: does this have to happen now, on this thread, before the first frame? A surprising amount of it can be moved — earlier to build time, later to after first frame, or sideways to a background thread — and each thing moved is pure win, because the user’s perception of speed is set entirely by what happens before they see and can touch the screen. Precompilation is just the most automated version of that same move.
Where to stop
Launch optimisation has diminishing returns like anything else. Once cold start is comfortably under the point where users perceive delay, further milliseconds are invisible and the deferral machinery you add is real complexity someone must maintain. The goal is a launch that feels instant on a representative device, not a benchmark number chased for its own sake.
The reliable path there is a mindset: the startup path is sacred, and everything competes to be on it. Initialise lazily, move work off the main thread, show a frame before you are fully loaded, and defer every SDK that does not need to be ready before the first screen. Do that and the three-second launch becomes a fast one — and the impression your app makes in its first second becomes a good one.