MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
I break down the biggest cold start bottlenecks in Android apps and show you how to measure, profile, and eliminate them with real code examples.
On this page
Cold start latency kills user retention before your first screen even renders. Every 100 milliseconds of delay increases abandonment risk. I have shipped 22 apps from Bangkok and learned that startup optimization is not a one-time task — it is a discipline you bake into every release.
The difference between a 1.2-second launch and a 2.8-second launch is often the difference between a 4.5-star rating and a 3.2-star rating. Users do not care about your architecture. They care that the app opens now.
A cold start happens when the system creates your process from scratch. The OS loads your application class, attaches the base context, runs
onCreate()| Phase | Typical Duration | What Happens |
|---|---|---|
| Process Creation | 50–150 ms | Zygote fork, memory allocation |
| Application.onCreate() | 100–500 ms | Your initialization code runs |
| Activity Creation | 50–200 ms | code code code |
| View Inflation & Layout | 100–400 ms | XML parsing, measure, layout, draw |
| First Frame Render | 16–50 ms | GPU composition, surface presentation |
[!NOTE] The launch theme window background renders before your activity code runs. A well-designed launch theme buys you perceived speed while real initialization happens in the background.
Your
Application.onCreate()setContentView()You cannot optimize what you do not measure. Android provides three complementary tools that I use on every project.
Macrobenchmark gives you production-representative numbers. It launches your app in a separate process, measures time-to-initial-display (TTID) and time-to-full-display (TTFD), and outputs JSON you can track in CI.
// app/src/androidTest/kotlin/com/sudarshantechlabs/startup/StartupBenchmark.kt
package com.sudarshantechlabs.startup
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.benchmark.macro.perfetto.PerfettoTrace
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun coldStartup_timeToInitialDisplay() {
benchmarkRule.measureRepeated(
packageName = "com.sudarshantechlabs.myapp",
metrics = listOf(StartupTimingMetric()),
iterations = 10,
startupMode = StartupMode.COLD,
setupBlock = { pressHome() }
) {
startActivityAndWait()
}
}
private fun pressHome() {
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
.pressHome()
}
private fun startActivityAndWait() {
val intent = Intent().apply {
setPackage("com.sudarshantechlabs.myapp")
setAction("android.intent.action.MAIN")
addCategory("android.intent.category.LAUNCHER")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.executeShellCommand("am start -W -S $intent")
device.wait(Until.hasObject(By.pkg("com.sudarshantechlabs.myapp").depth(0)), 5000)
}
}Perfetto traces show you exactly what runs on each thread during startup. Open the trace in
ui.perfetto.devApplication.onCreate()ContentProviderBaseline Profiles let ART pre-compile your hot paths. Since Android 12 (API 31), they are the highest-leverage optimization you can ship. A good baseline profile reduces cold start by 20–40% without changing a line of app logic.
// baseline-profiles/src/main/kotlin/com/sudarshantechlabs/baseline/StartupProfileGenerator.kt
package com.sudarshantechlabs.baseline
import androidx.benchmark.macro.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class StartupProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()
@Test
fun generate() {
baselineProfileRule.collect(
packageName = "com.sudarshantechlabs.myapp"
) {
startActivityAndWait()
// Scroll through key lists to capture RecyclerView layout code
device.findObject(By.res("com.sudarshantechlabs.myapp:id/home_list"))
.fling(Direction.DOWN)
device.waitForIdle()
// Navigate to second screen to capture fragment transitions
device.findObject(By.text("Settings")).click()
device.waitForIdle()
}
}
}[!TIP] Generate baseline profiles for every release. Add the
Gradle module, run the generator on a physical device (not emulator), and ship thecodebaseline-profilesin your AAB. The Play Store delivers it to users automatically.codebaseline-prof.txt
The
ApplicationonCreate()Lazy initialization moves work off the startup path. Use
by lazylateinitProvider<T>// app/src/main/kotlin/com/sudarshantechlabs/myapp/MyApplication.kt
package com.sudarshantechlabs.myapp
import android.app.Application
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.ProcessLifecycleOwner
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.sudarshantechlabs.myapp.di.AppContainer
import com.sudarshantechlabs.myapp.repository.UserRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
class MyApplication : Application() {
// Initialized on first access, not in onCreate()
val appContainer by lazy { AppContainer(this) }
// Coroutine scope tied to process lifecycle
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
override fun onCreate() {
super.onCreate()
// Only the absolute minimum runs here
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
// Crashlytics must initialize early for crash capture
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
// Defer everything else to background
applicationScope.launch(Dispatchers.IO) {
initializeDeferredComponents()
}
}
private suspend fun initializeDeferredComponents() {
// Analytics, database, network clients, image loaders — all here
val analytics = FirebaseAnalytics.getInstance(this@MyApplication)
analytics.setAnalyticsCollectionEnabled(true)
// Warm up repositories that need disk I/O
appContainer.userRepository.warmUp()
// Pre-load critical configs
appContainer.configRepository.preload()
}
}[!WARNING] Never perform disk I/O, network calls, or complex calculations in
on the main thread. The system kills your process with an ANR if you block longer than ~5 seconds. Move all non-essential work tocodeApplication.onCreate()orcodeDispatchers.IO.codeWorkManager
ContentProviders initialize before
Application.onCreate()FirebaseInitProvider<!-- app/src/main/AndroidManifest.xml -->
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
android:exported="false"
android:initOrder="100" />
<!-- Disable auto-init for Analytics -->
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
<!-- Disable auto-init for Crashlytics -->
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />Activity launch optimization starts with your theme. Use a window background drawable that matches your first frame. Avoid
Theme.AppCompat.Light.NoActionBar<!-- res/drawable/launch_background.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/brand_dark_background" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/ic_launcher_foreground" />
</item>
</layer-list><!-- res/values/themes.xml -->
<style name="Theme.Launch" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowIsTranslucent">false</item>
<item name="android:windowDisablePreview">true</item>
</style>Apply
Theme.LaunchNot everything needs to run at startup. Feature flags, remote config, periodic sync, log uploads — these belong in
WorkManagerExistingPeriodicWorkPolicy.KEEPREPLACE// app/src/main/kotlin/com/sudarshantechlabs/myapp/worker/StartupDeferredWorker.kt
package com.sudarshantechlabs.myapp.worker
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkManager
import androidx.work.WorkRequest
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import com.sudarshantechlabs.myapp.repository.AnalyticsRepository
import com.sudarshantechlabs.myapp.repository.RemoteConfigRepository
import kotlinx.coroutines.Dispatchers
import java.util.concurrent.TimeUnit
class StartupDeferredWorker(
context: Context,
params: WorkerParameters,
private val analyticsRepository: AnalyticsRepository,
private val remoteConfigRepository: RemoteConfigRepository
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// Fetch remote config for feature flags
remoteConfigRepository.fetchAndActivate()
// Upload pending analytics events
analyticsRepository.flushPendingEvents()
// Check for app updates
checkForUpdates()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
private suspend fun checkForUpdates() {
// In-app update check logic here
}
companion object {
const val WORK_NAME = "StartupDeferredWork"
const val INTERVAL_HOURS = 24L
fun schedule(context: Context) {
val workRequest = PeriodicWorkRequestBuilder<StartupDeferredWorker>(
INTERVAL_HOURS, TimeUnit.HOURS
)
.setInitialDelay(5, TimeUnit.MINUTES) // Don't compete with startup
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
workRequest
)
}
}
}Schedule this worker from your deferred initialization block, not from
Application.onCreate()// In MyApplication.initializeDeferredComponents()
StartupDeferredWorker.schedule(this@MyApplication)[!IMPORTANT] Report
when your first meaningful content is visible. This tells the system your app is interactive, triggering ART profile compilation and improving subsequent launches. Call it after your RecyclerView adapter submits data, not incodereportFullyDrawn().codeonResume()
// In your MainActivity or HomeFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.items.observe(viewLifecycleOwner) { items ->
adapter.submitData(items)
// Report fully drawn AFTER data renders
reportFullyDrawn()
}
}Application.onCreate()Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read