Note: This article was completed by AI (Claude Opus 4.7) from my initial notes and thoughts.
Our app pulls in a lot of components — some we wrote ourselves, others from third-party libraries — and every so often they need to be initialized at startup. On Android, the default habit is to drop those initialize() calls into Application.onCreate().
class AppApplication : Application() {
override fun onCreate() {
super.onCreate()
AnalyticsSdk.initialize(this)
CrashReporter.initialize(this)
ImageLoader.initialize(this)
ExperimentClient.initialize(this)
// ... and so on
}
}
Looks harmless, right? Unfortunately it makes cold launch drag. On our project, Firebase Performance measured cold launch at about three seconds — which means the user taps the icon and waits three seconds before seeing anything. Not even the splash screen.
onCreate() is so sensitiveApplication.onCreate() runs on the main thread, and it runs before the system can draw any UI you own. During that window the user sees the OS's blank starting window. Every extra initialize() call adds directly to that delay.
Worse, SDKs rarely advertise how long their initializers take. An innocuous-looking call may do disk I/O, read SharedPreferences, or even block on a synchronous network request.
Not every module needs to be initialized in onCreate(). I sort them into three buckets:
onCreate(), but pick lightweight SDKs.onCreate() with Thread {} or Executors.newSingleThreadExecutor() — don't block the main thread.Figuring out which bucket each SDK belongs in is the biggest single win.
For bucket 3, the simplest trigger is the first Activity.onResume():
class MainActivity : AppCompatActivity() {
private var initialized = false
override fun onResume() {
super.onResume()
if (!initialized) {
initialized = true
window.decorView.post {
AnalyticsSdk.initialize(application)
ImageLoader.initialize(application)
}
}
}
}
Using decorView.post {} means the work runs after the current frame finishes drawing, so it doesn't compete with the first frame for resources.
If you want something more structured, Jetpack's App Startup library provides an Initializer interface that centralizes dependency ordering and supports lazy initialization:
class AnalyticsInitializer : Initializer<AnalyticsSdk> {
override fun create(context: Context): AnalyticsSdk {
return AnalyticsSdk.initialize(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
Mark it as lazy in AndroidManifest.xml so it doesn't run with Application.onCreate():
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.AnalyticsInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
Then call AppInitializer.getInstance(context).initializeComponent(AnalyticsInitializer::class.java) whenever you actually need it.
Pushing init to a background thread sounds simple, but there are two common traps:
CompletableFuture (or Deferred, or StateFlow) so callers can await completion.Thread competes with the main thread for CPU, which barely helps on low-end devices. Use Thread.MIN_PRIORITY or Executors.newSingleThreadExecutor() with an explicit priority.Before any of this, measure the baseline. Firebase Performance's "App start" trace, or adb shell am start -W, will give you a starting number. Measure again after. Without numbers you're guessing — and guessing usually flatters yourself.
On our project, moving bucket 3 off the critical path meaningfully reduced cold launch time and brought the user's time-to-first-pixel down with it.
Application.onCreate() runs on the main thread and blocks the first frame. Don't treat it as a universal init hook.decorView.post {} or Jetpack App Startup's lazy initializer.