App Startup Time Explained
PERFORMANCE › Runtime
Android launches come in three flavors, and the whole topic hinges on knowing what each one reuses versus rebuilds from scratch:
- **Cold start**: the app's process doesn't exist yet. The system forks a new process, creates the Application object, runs content providers, creates the main Activity, inflates and lays out views, then draws the first frame. This is the expensive one, and the one Android Vitals measures. - **Warm start**: the process is still alive, but the Activity was destroyed (for example after onTrimMemory reclaimed it), so it goes through onCreate() again, no process fork, no new Application object. - **Hot start**: the Activity is still resident in memory, the system just brings it back to the foreground. No object init, no inflation, this is the cheapest launch by far.
Interviewers want you to reason about *which work each state repeats*, not just recite the names.
During a **cold start**, before your Application.onCreate() code runs, the framework has already done real work: it instantiates every declared ContentProvider and calls its onCreate(), right after attachBaseContext() but before Application.onCreate().
1. Application.attachBaseContext()
2. ContentProvider.onCreate() <- runs for EVERY provider, library auto-init lands here
3. Application.onCreate() <- your code runs after all providers
This matters because plenty of libraries historically shipped a content provider purely to auto-initialize themselves. Each one adds startup cost, and with no defined ordering between them, this is exactly the problem the AndroidX App Startup library was built to fix, more on that shortly.
Two metrics matter for launch speed, and only one of them is free:
- **TTID (Time To Initial Display)**: how long until the first frame draws. The framework reports this automatically, you'll see it as a Displayed line in logcat, no app code required. - **TTFD (Time To Full Display)**: how long until the app is actually usable, including async content like a network-loaded feed. The system has no way to know when that's done, so *you* have to tell it by calling reportFullyDrawn().
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadDataAsync { data ->
populateUi(data)
reportFullyDrawn() // marks TTFD once real content is visible
}
}
}
A fast TTID with a slow TTFD is a common trap: the first frame looks instant, but the screen is a spinner for three more seconds.
Back to the content-provider problem: the **AndroidX App Startup library** replaces N separate per-library providers with exactly one, InitializationProvider, which runs a set of Initializer<T> components you register in the manifest.
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false">
<meta-data android:name="com.example.AnalyticsInitializer" android:value="androidx.startup" />
</provider>
class AnalyticsInitializer : Initializer<AnalyticsSdk> {
override fun create(context: Context): AnalyticsSdk = AnalyticsSdk.init(context)
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
One provider means one instantiation cost instead of many, and Initializer gives you a documented, ordered way to declare "this needs that to run first", instead of relying on undefined provider ordering.
Even with a single provider, some initializers are only needed occasionally, why pay their cost on every cold start? You can opt an Initializer out of automatic startup by removing its meta-data entry with tools:node="remove", then trigger it yourself on demand:
<!-- excluded from the automatic InitializationProvider pass -->
<meta-data android:name="com.example.HeavyInitializer" tools:node="remove" />
// runs it (and its declared dependencies) lazily, whenever you actually need it
AppInitializer.getInstance(context)
.initializeComponent(HeavyInitializer::class.java)
This is the same lazy-initialization principle that applies everywhere in startup work: anything that isn't needed to draw the first frame is a candidate to defer until after launch, or until the feature that actually needs it is used.
**Baseline profiles** attack a different part of the problem: how fast the *code itself* runs on first launch. Without one, ART interprets or JIT-compiles your app's hot methods the first time they execute, which is slower than running compiled native code. A baseline profile is a list of hot classes or methods shipped in the app, and ProfileInstaller installs it so ART **AOT-compiles** exactly those methods at install time.
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule val rule = BaselineProfileRule()
@Test
fun generate() = rule.collect(packageName = "com.example.app") {
pressHome()
startActivityAndWait() // walks the cold-start path; those methods get AOT-compiled
}
}
The generated baseline-prof.txt ships inside the app and commonly cuts a meaningful chunk off cold start, without you touching a line of your app's actual startup logic.
In **Jetpack Compose**, you don't call reportFullyDrawn() from inside a composable body, doing so on every recomposition would misfire constantly. Instead Compose gives you purpose-built helpers that call through to it at the right moment:
@Composable
fun HomeScreen(items: List<Item>?) {
if (items == null) {
CircularProgressIndicator()
} else {
LazyColumn { items(items) { ItemRow(it) } }
}
ReportDrawnWhen { items != null } // calls reportFullyDrawn() once true
}
ReportDrawnWhen fires once a condition becomes true, ReportDrawnAfter runs a suspend block first, and ReportDrawn fires unconditionally on first composition. For screens with multiple concurrent async loads, FullyDrawnReporter's addReporter or removeReporter let you require several conditions before signalling TTFD.