App startup time directly affects user retention. Google's internal research shows that apps taking more than 5 seconds to start lose a significant percentage of users. There is a direct relationship between startup time and Play Store ratings.
Understanding the three start types and where time is spent gives you a concrete plan for improvement.
Three Start Types
Cold start: The process does not exist. Android creates a new process, initializes the Application class, creates the first Activity, and draws the first frame. This is the slowest start. Optimization here has the most user impact.
Warm start: The process exists but the Activity was destroyed (e.g. the user navigated away). Android recreates the Activity but skips process creation.
Hot start: The process and Activity both exist (the user just switched away). Android brings the window to the foreground. Optimization here has minimal impact.
Measuring Startup
Never optimize what you cannot measure.
Reported Display Time — Android logs the time from process creation to first drawn frame:
ActivityTaskManager: Displayed com.example/.MainActivity: +1s234ms
Macrobenchmark — The recommended way to measure startup in CI:
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
}
Run this on a real device (not an emulator) for accurate numbers.
What Makes Startup Slow
-
Heavy
Application.onCreate()— Initializing analytics SDKs, crash reporters, and third-party libraries synchronously on the main thread. -
Slow
ContentProviderinitialization — Content providers are initialized beforeApplication.onCreate. Hidden initializations viaStartuplibrary or library-provided providers add time invisibly. -
Main thread I/O — Reading from SharedPreferences, database, or network on the main thread.
-
Long inflation/composition — Complex first screen with many composables or deeply nested views.
Lazy Initialization
Move non-critical work off the startup path.
class App : Application() {
override fun onCreate() {
super.onCreate()
// Critical: must be ready before any screen shows
initializeCrashReporter()
// Defer: analytics can wait until the user interacts
MainScope().launch {
delay(3000)
initializeAnalytics()
}
}
}
Use by lazy for singletons that are not always needed:
val heavyClient: HeavyApiClient by lazy { HeavyApiClient.create() }
App Startup Library
The App Startup library provides a structured way to initialize components in sequence with dependency management, replacing multiple ContentProvider registrations.
class AnalyticsInitializer : Initializer<Analytics> {
override fun create(context: Context): Analytics {
return Analytics.initialize(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
Declare in the manifest:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup">
<meta-data
android:name="com.example.AnalyticsInitializer"
android:value="androidx.startup.Initializer" />
</provider>
To defer an initializer, call AppInitializer.getInstance(context).initializeComponent(AnalyticsInitializer::class.java) when you actually need it.
Baseline Profiles
Baseline Profiles tell the Android Runtime (ART) which code paths to ahead-of-time compile before the user ever runs them. This eliminates JIT compilation overhead on first run.
Generate a Baseline Profile with the Macrobenchmark library:
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val baselineRule = BaselineProfileRule()
@Test
fun generate() = baselineRule.collect(
packageName = "com.example.app"
) {
pressHome()
startActivityAndWait()
// walk critical user journeys
}
}
The generated baseline-prof.txt file goes in src/main/. The AGP bundles it into the release APK/AAB. Google Play compiles these methods during installation, so users get fast startup on first launch.
Splash Screen API
Use the SplashScreen API (API 31+, backwards-compatible via core-splashscreen) instead of a custom launch Activity. It shows the app icon on the system-drawn splash, hides it when the first frame is drawn, and avoids a full extra Activity in the back stack.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
// Keep splash visible while data loads
splashScreen.setKeepOnScreenCondition { viewModel.isLoading.value }
setContent { App() }
}
}
Common Wins Summary
| Optimization | Typical gain |
|---|---|
| Defer analytics SDK init | 200–600 ms |
| Baseline Profiles | 20–40% on first cold start |
| Lazy heavy singletons | 100–400 ms |
| Replace ContentProviders with App Startup | 50–200 ms |
| Move SharedPreferences reads off main thread | 50–300 ms |
Practice
Profile your app's cold start with the Macrobenchmark library. Then add a method trace in Application.onCreate() using Trace.beginSection and Trace.endSection to find the slowest initializer. Move it to a lazy {} block and measure again.
Summary
Optimize cold start by deferring non-critical work, using Baseline Profiles for ART compilation, moving I/O off the main thread, and measuring with Macrobenchmark rather than eyeballing. Every 100ms you save reduces churn, especially on low-end devices.