Cold start time is one of the most-watched metrics in mobile: Google Play uses it in app quality ratings, and users abandon apps that take more than 2 seconds to show content. This lesson covers the tools and techniques to measure and improve it.
Startup Timeline
User taps icon
└── Zygote fork (~5–10ms, not controllable)
└── attachBaseContext
└── ContentProvider.onCreate (ALL providers, synchronous)
└── Application.onCreate
└── Activity.onCreate
└── Activity.onStart/onResume
└── First frame rendered ← TTID (Time to Initial Display)
└── Content loaded ← TTFD (Time to First Draw)
Everything between process creation and first frame is "startup." Most optimization happens in ContentProvider.onCreate and Application.onCreate.
Measuring Startup
# Method 1: adb am start -W
adb shell am force-stop com.example.myapp
adb shell am start-activity -W -n com.example.myapp/.MainActivity
# Output: TotalTime: 1234ms ← this is your baseline
# Method 2: logcat
adb logcat | grep "Displayed"
# I/ActivityTaskManager: Displayed com.example.myapp/.MainActivity: +1s234ms
Also check Android Vitals in Play Console — real user cold start P50/P95 percentiles.
App Startup Library
Jetpack's App Startup library replaces multiple ContentProviders (each adds ~10–30ms overhead) with a single provider that initializes all your components lazily or eagerly via Initializer:
// Before: each SDK has its own ContentProvider — 3 providers = ~60ms overhead
// WorkManager's old approach, Firebase's approach, etc.
// After: all initializers share one ContentProvider
class WorkManagerInitializer : Initializer<WorkManager> {
override fun create(context: Context): WorkManager {
val config = Configuration.Builder().setMinimumLoggingLevel(Log.ERROR).build()
WorkManager.initialize(context, config)
return WorkManager.getInstance(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
class AnalyticsInitializer : Initializer<AnalyticsClient> {
override fun create(context: Context): AnalyticsClient {
return AnalyticsClient.init(context)
}
override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}
<!-- AndroidManifest.xml — replaces multiple SDK ContentProviders -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false">
<meta-data
android:name="com.example.WorkManagerInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.example.AnalyticsInitializer"
android:value="androidx.startup" />
</provider>
Lazy Initialization
If a component isn't needed until the user interacts, don't initialize it at startup:
// ❌ Initialized unconditionally at startup
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
HeavySdk.initialize(this) // ~200ms, always paid
Analytics.init(this) // ~100ms, always paid
}
}
// ✅ Lazy — initialized on first access
class MyApplication : Application() {
val heavySdk: HeavySdk by lazy { HeavySdk.initialize(this) }
// Analytics only initialized when user triggers a tracked event
}
Defer Non-Critical Work
// Defer work that doesn't affect first frame
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Critical: must run synchronously
CrashReporting.init(this)
// Non-critical: defer to after first frame
lifecycleScope.launch {
// Wait until the main Activity is resumed
ProcessLifecycleOwner.get().lifecycle.withResumed {
Analytics.init(this@MyApplication)
PushNotifications.init(this@MyApplication)
}
}
}
}
ProfileInstaller (Baseline Profiles)
Baseline Profiles pre-compile the hot paths in your app so ART doesn't JIT-compile them at first run:
// build.gradle.kts
dependencies {
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
}
Generate with Macrobenchmark:
@RunWith(AndroidJUnit4ClassRunner::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() = rule.collect("com.example.myapp") {
pressHome()
startActivityAndWait()
// Navigate through critical flows
}
}
Common Startup Bottlenecks
| Bottleneck | Typical cost | Fix |
|---|---|---|
| Firebase init ContentProvider | 50–100ms | Disable auto-init; use lazy init |
| Room.build() on main thread | 50–80ms | Build DB on background thread |
| Network call in Application.onCreate | Variable | Never do this |
| SharedPreferences on main thread | 10–50ms | Use DataStore with coroutines |
| Large dex class loading | 20–100ms | Baseline Profiles |