Understanding Cold, Warm, and Hot Starts
Measuring Startup with Macrobenchmark
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun coldStartup() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
iterations = 10,
startupMode = StartupMode.COLD,
) {
pressHome()
startActivityAndWait()
}
@Test
fun coldStartupWithBaselineProfile() =
benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
iterations = 10,
startupMode = StartupMode.COLD,
compilationMode = CompilationMode.Partial(
baselineProfileMode =
BaselineProfileMode.Require
),
) {
pressHome()
startActivityAndWait()
}
}Baseline Profiles: The Single Biggest Win
// benchmark/src/main/java/BaselineProfileGenerator.kt
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generateBaselineProfile() = rule.collect(
packageName = "com.example.app",
) {
// Cold start the app
pressHome()
startActivityAndWait()
// Navigate through critical user journeys
device.findObject(By.text("Home"))
.click()
device.waitForIdle()
device.findObject(By.text("Search"))
.click()
device.waitForIdle()
device.findObject(By.text("Profile"))
.click()
device.waitForIdle()
}
}
// app/build.gradle.kts
plugins {
id("com.android.application")
id("androidx.baselineprofile")
}
dependencies {
baselineProfile(project(":benchmark"))
}
baselineProfile {
automaticGenerationDuringBuild = true
}Lazy Initialization with the App Startup Library
// Eagerly initialized via App Startup manifest merge
class AnalyticsInitializer : Initializer<Analytics> {
override fun create(context: Context): Analytics {
return Analytics.init(context, BuildConfig.ANALYTICS_KEY)
}
override fun dependencies(): List<Class<out Initializer<*>>> =
emptyList()
}
// Deferred until after first frame
class CrashReportingInitializer : Initializer<CrashReporter> {
override fun create(context: Context): CrashReporter {
return CrashReporter.init(context)
}
// Depends on analytics being ready first
override fun dependencies(): List<Class<out Initializer<*>>> =
listOf(AnalyticsInitializer::class.java)
}
// In Application.onCreate() -- defer non-critical work
class App : Application() {
override fun onCreate() {
super.onCreate()
// Critical: initialize immediately
AppInitializer.getInstance(this)
.initializeComponent(AnalyticsInitializer::class.java)
// Deferred: initialize after first frame
val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post {
AppInitializer.getInstance(this)
.initializeComponent(
CrashReportingInitializer::class.java
)
}
}
}Reducing Layout Complexity on the First Frame
// Instead of loading everything on first frame:
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Column {
// Lightweight header -- renders immediately
TopBar(title = "Home")
when (uiState) {
is HomeState.Loading -> {
// Minimal placeholder -- fast first frame
ShimmerLoadingList(itemCount = 6)
}
is HomeState.Success -> {
// Heavy content loads after first frame
LazyColumn {
items(uiState.items) { item ->
ContentCard(item)
}
}
}
is HomeState.Error -> {
ErrorBanner(uiState.message)
}
}
}
}
// Shimmer placeholder -- renders in <1ms, no data needed
@Composable
fun ShimmerLoadingList(itemCount: Int) {
LazyColumn {
items(itemCount) {
ShimmerCard(modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
}Diagnosing Bottlenecks with Perfetto
Key Takeaways
- 1Baseline Profiles deliver 20-40% faster cold starts with minimal code change -- implement them first.
- 2Measure with Macrobenchmark before and after every optimization to avoid regression.
- 3The App Startup library eliminates redundant ContentProviders and lets you defer non-critical initialization.
- 4Keep the first frame lightweight: show placeholders, load data asynchronously, flatten layout hierarchy.
- 5Use Perfetto to find main-thread blocking work before the first frame draws.
- 6Target under 500ms TTID for cold start -- users perceive anything longer as slow.
Frequently Asked
What's a good cold start time?
Under 500ms is excellent. 500ms-1s is acceptable. Over 1s needs optimization. Measure on mid-range devices, not just flagship phones.
How do Baseline Profiles help?
ART compiles specified methods during app installation, avoiding JIT compilation at runtime. This eliminates compilation pauses during startup.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.