Opening
You tap an icon on your phone. A fraction of a second later, an app appears. It feels instant — or at least it should. But have you ever stopped to wonder what actually happens in that fraction of a second? Because it is not simple. Between your finger touching glass and pixels appearing on screen, your phone executes an extraordinary sequence of operations. Processes are forked. Memory is allocated. Classes are loaded. Layouts are inflated. Pixels are drawn. And all of it has to happen fast enough that you do not notice. Today we are going to trace that entire journey — from the moment your finger lifts off the icon to the moment the first frame appears on screen. And once you see all the steps, you will understand exactly why some apps launch in 200 milliseconds and others take two full seconds.
What Happens When You Tap
So you tap an app icon. What is the very first thing that happens? Your tap is captured by the system's input dispatcher, which figures out what you tapped on. It sees you tapped a launcher icon, so it sends an intent to the system server — specifically to the ActivityManagerService. The ActivityManagerService is the brain of Android's app lifecycle. It looks at that intent and asks: is there already a process running for this app? If yes, great — we might be able to take a shortcut. If no, we need to start from scratch. And starting from scratch means we need a brand new Linux process. But Android does not create processes the way a normal Linux system would. It has a trick. A very clever trick called Zygote.
Zygote: The Master Process
Zygote is a special process that starts when your phone boots up. Its entire job is to be ready to give birth to new app processes. And the reason it exists is performance. Creating a process from scratch — loading the entire Android runtime, preloading thousands of framework classes, setting up the graphics system — takes a long time. So Android does all of that once, at boot, in Zygote. Zygote preloads every common class, every common resource, everything apps typically need. Then when you tap an icon and the system needs a new process, Zygote does not start from zero. It forks itself. Fork is a Linux system call that creates an exact copy of the current process. It is extremely fast because of copy-on-write memory — the new process shares Zygote's memory pages until it actually modifies something. So your new app process starts life as a clone of Zygote, with the entire Android framework already loaded and ready. This is one of the most important performance optimizations in all of Android.
From Fork to Application.onCreate
Once Zygote has forked a new process for your app, the runtime needs to set up your specific application. It loads your APK, finds your Application class, and calls its constructor followed by onCreate. This is the first code of yours that runs. And here is the critical thing — everything that happens in Application.onCreate happens before your user sees anything. Every library you initialize, every SDK you configure, every analytics framework you set up — all of it adds directly to your startup time. I have seen real production apps where Application.onCreate takes 800 milliseconds. Almost a full second before any UI work even begins. That is just library initialization. Dagger setting up dependency injection. Firebase initializing. Crash reporters hooking in. Advertising SDKs phoning home. Every single one of those milliseconds is time your user is staring at a blank screen.
Activity Creation and the First Layout
After Application.onCreate finishes, the system creates your launcher Activity and calls its onCreate. This is where you typically call setContentView or, in Compose, setContent. When you call setContentView, the system inflates your XML layout — parsing the XML, creating View objects, and assembling them into a tree. Each View in that tree needs to be constructed, its attributes need to be read, and it needs to be attached to its parent. For a complex layout with dozens of views, this is real work. Then the system needs to do a measure pass — figuring out how big each View should be. Then a layout pass — figuring out where each View goes. And finally a draw pass — actually rendering pixels. Only when that first draw completes does the user see your app. That moment — the first frame appearing on screen — is what we call Time to Initial Display, or TTID. From tap to first frame. That is the number that matters.
Cold Start vs Warm Start vs Hot Start
Now, not every app launch goes through all those steps. Android defines three types of startup. A cold start is the full journey — no process exists, Zygote forks, Application.onCreate runs, Activity is created from scratch. This is the slowest path. A warm start means your process is still alive but the Activity was destroyed. So we skip process creation and Application.onCreate, but we still need to create the Activity and render it. A hot start is the fastest — your process is alive and your Activity is still in memory. The system just brings it to the foreground. Maybe it calls onResume, but there is minimal work to do. Understanding these three modes is essential because when you measure startup time, you need to know which mode you are measuring. A hot start of 100 milliseconds tells you nothing about your cold start, which might be 1500 milliseconds.
What Makes Startup Slow
Let us talk about the common killers. The number one culprit I see in real apps is a heavy Application.onCreate. Too many libraries initialized eagerly on the main thread. The fix is lazy initialization — do not initialize something until you actually need it. Use App Startup library or just write your own lazy providers. The second culprit is disk IO on the main thread during startup. Reading SharedPreferences, loading configuration files, reading from a database. Every disk read blocks your main thread and adds directly to startup time. SharedPreferences is particularly sneaky because it loads the entire file into memory on first access, and if you access it on the main thread during startup, you are blocking. The third culprit is large view hierarchies. A deeply nested layout with many views takes longer to inflate, measure, layout, and draw. Each level of nesting can trigger multiple measurement passes. Flat layouts with ConstraintLayout or Compose are significantly faster.
Measuring Startup Time
How do you actually measure this? The simplest tool is adb shell am start with the dash capital W flag. This command launches your app and prints three timing numbers: ThisTime, TotalTime, and WaitTime. TotalTime is the one you usually care about — it measures from when the system received the launch intent to when your first frame finished drawing. Run this multiple times and take the median, because startup times vary. For more detailed analysis, you can use Android Studio's profiler. Start a trace before launching your app, and you will see exactly where every millisecond goes. You can see Application.onCreate, Activity.onCreate, the measure pass, the layout pass, the draw — all laid out on a timeline. This is how you find the specific bottleneck. Is it library init? Disk IO? Layout inflation? The profiler will tell you.
Baseline Profiles
Now here is something that was a game changer when it arrived. Baseline profiles. Remember from earlier in this course how ART uses JIT compilation — it interprets bytecode at first, then compiles hot methods to native code over time? Well, during a cold start, none of your code has been JIT compiled yet. The runtime is interpreting bytecode, which is significantly slower than running compiled native code. Baseline profiles solve this. A baseline profile is a list of classes and methods that should be pre-compiled to native code at install time, using AOT compilation. You generate the profile by running your app through its critical user journeys — startup, navigation, key screens — and the Macrobenchmark library records which methods were executed. Then that profile ships with your APK, and when the user installs your app, ART pre-compiles all those methods. The result? On your next cold start, the hot paths are already native code. No JIT warmup needed. Google reports that baseline profiles can improve startup time by up to 30 percent. That is massive.
The Startup Timeline Visualized
Let me put the whole timeline together so you can see it as one sequence. Time zero: user taps the icon. The system resolves the intent and sees no process exists. It asks Zygote to fork. That fork takes roughly 5 to 10 milliseconds. Then Application.onCreate runs — in a well-optimized app, maybe 50 to 100 milliseconds. Then Activity.onCreate fires. You inflate your layout and set up your views. Then the system does measure, layout, and draw. And finally, the first frame is submitted to the display. For a well-optimized app, that total cold start should be under 500 milliseconds. For a complex app, under one second is a reasonable target. The Android Vitals dashboard will flag your app if cold start exceeds 5 seconds, but honestly, if you are anywhere near 5 seconds, you have a serious problem. Users perceive delays over one second as sluggish.
Practical Tips
Let me leave you with concrete tips you can apply right away. One: audit your Application.onCreate. List every library you initialize and ask — does this need to happen before the first frame? If not, defer it. Two: move disk IO off the main thread. If you read SharedPreferences during startup, switch to DataStore or at least access preferences on a background thread. Three: simplify your initial layout. Your first screen does not need to show everything immediately. Load the skeleton, show it to the user, then populate details asynchronously. Four: use baseline profiles. Set up the Macrobenchmark library, record your startup profile, and include it in your build. Five: measure, measure, measure. Use adb shell am start dash capital W as part of your development workflow. Know your cold start number the way you know your APK size.
Recap and What is Next
Let us recap. When you tap an icon, the system asks Zygote to fork a new process. Your Application.onCreate runs, then your Activity is created, the layout is inflated, measured, laid out, and drawn. The first frame appears on screen, and only then does the user see your app. Cold starts go through every step. Warm starts skip process creation. Hot starts skip almost everything. The biggest startup killers are heavy initialization, disk IO on the main thread, and complex view hierarchies. Baseline profiles pre-compile your hot paths to eliminate JIT warmup. In the next video, we are going to zoom into the main thread itself — the message loop that powers everything you see on screen. You will learn about Looper, Handler, MessageQueue, and why blocking the main thread freezes your entire app. See you there.