Cutting Android Cold Start from 3s to 800ms with Measurement, Not Magic
Android Cold Start Optimization in Practice: The Investigation Path from 3 Seconds to 800ms
Author: hunterandroid Tags: Frontend, Android
User feedback that "the app opens slowly" is the most common yet hardest-to-quantify performance problem. This article reviews a real optimization process: squeezing cold start from 3.1 seconds down to around 800ms — the method is more worth copying than the result.
1. First, clarify what "cold start" means
Cold start = process does not exist → start process → load Application → create first Activity → first frame rendering complete.
Three key nodes:
- T0:
am_proc_start— the system begins pulling the process - T1:
Application.onCreatefinishes - T2: the first Activity's
onWindowFocusChanged(true)fires
What we want to compress is T2 - T0. Every step in between can be the culprit.
2. Measurement: optimization without measurement is metaphysics
Trust only one kind of data: adb shell am start -W.
adb shell am start -W -n com.demo.app/.MainActivity
In the output, pay attention to:
ThisTime: Activity launch durationTotalTime: total duration including ApplicationWaitTime: AMS-layer waiting time
Layer on in-code instrumentation:
class App : Application() {
override fun onCreate() {
val t0 = SystemClock.elapsedRealtime()
super.onCreate()
// ... initialization
val cost = SystemClock.elapsedRealtime() - t0
Log.i("Startup", "App.onCreate cost=${cost}ms")
}
}
On the Activity side, use Choreographer.getInstance().postFrameCallback or reportFullyDrawn() to capture the first frame.
3. Application layer: three moves
In this round of optimization, Application went from 1.4s down to 380ms, relying on three things.
1. Tiered initialization
Split SDKs into three categories: "must be synchronous" / "can be deferred" / "can be background":
override fun onCreate() {
super.onCreate()
// Must be synchronous
initCrashReport()
initLogger()
// Run after main thread is idle
Looper.myQueue().addIdleHandler {
initAnalytics()
initImageLoader()
false
}
// Background thread
Thread {
initPushSDK()
initABTest()
}.start()
}
Key point: IdleHandler should only run work the main thread can tolerate. Don't stuff time-consuming SDKs into it, or you just move the lag from launch to first-screen interaction.
2. Defer until after the first screen
Use the App Startup library or hand-write a ContentProvider trigger — the core idea is to push initialization that the first screen doesn't depend on until after Activity onResume:
class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
if (!inited) {
window.decorView.post {
initSecondaryModules()
inited = true
}
}
}
}
3. Kill implicit ContentProvider initialization
Third-party SDKs often use <provider> to quietly run code during startup. Check with:
adb shell dumpsys package com.demo.app | grep -A2 "Provider"
When you see unnecessary Providers, disable them with tools:node="remove" and then manually lazy-load:
<provider
android:name="com.some.sdk.InitProvider"
android:authorities="${applicationId}.some.init"
tools:node="remove" />
This step alone saved 220ms. Many SDK Providers don't actually need to be tied to startup at all.
4. First-screen Activity: layout and data both need slimming
Once Application optimization was in place, the Activity's 900ms became the bottleneck.
1. Layout hierarchy and measure/layout
Use Layout Inspector or hierarchyviewer to examine the first-screen hierarchy. Two pitfalls hit this time:
- A
ConstraintLayoutnested 5 layers ofLinearLayoutinside it; refactoring to a flat Constraint saved 90ms - An
includepulled in a huge generic header layout where 90% of views werevisibility=gone; switching toViewStublazy loading saved 60ms
2. Main-thread data sources must be async
Don't synchronously wait for first-screen-dependent API calls inside onCreate. Use Flow + loading skeleton screen:
class HomeViewModel : ViewModel() {
val state = MutableStateFlow<UiState>(UiState.Loading)
init {
viewModelScope.launch {
state.value = try {
UiState.Success(repository.load())
} catch (e: Exception) {
UiState.Error(e)
}
}
}
}
The Activity side renders the skeleton first, then refreshes when data arrives. What users perceive as "launch complete" is not data complete — it's the first frame being visible.
3. Don't use an Activity for the theme splash
Old code used SplashActivity → MainActivity navigation, and just creating the Activity cost an extra 150ms. Switch to a Theme-layer windowBackground:
<style name="AppTheme.Launcher" parent="AppTheme">
<item name="android:windowBackground">@drawable/launch_bg</item>
</style>
MainActivity uses the Launcher theme, then immediately switches back to AppTheme after super.onCreate. Saving one Activity is a guaranteed win.
5. Verification and regression prevention
If you don't guard against regression after optimization, you'll be back to square one within three months. Add two gates:
1. Add a startup duration baseline in CI
Run am start -W 20 times, take P90, and fail the build if it exceeds the baseline by 15%. This script can be placed in GitHub Actions or Jenkins.
2. Online monitoring
Report the duration from Application.attachBaseContext to onWindowFocusChanged, broken down by device model percentiles. Low-end device P90 is the real experience — don't just look at averages.
object StartupTracker {
private var t0 = 0L
fun mark() { t0 = SystemClock.elapsedRealtime() }
fun report(tag: String) {
val cost = SystemClock.elapsedRealtime() - t0
Analytics.log("startup", mapOf("tag" to tag, "cost" to cost))
}
}
6. Review: which optimizations "look nice" but aren't
Several approaches require caution:
- Multi-dex optimization / MultiDex.install ahead of time: with minSdk ≥ 21, you basically don't need to bother
- Turning off StrictMode: this just hides the problem; don't turn it off — keep it off in release, on in debug
- Reflection to call system APIs to skip initialization: the compatibility cost far outweighs the benefit
- Preloading classes ahead of time: not very meaningful; the difference is negligible after dex2oat
What really delivers immediate results is still those three things: reduce synchronous work in Application, flatten the first-screen layout, replace SplashActivity with a theme splash.
Summary
Cold start optimization has no magic: measure → decompose → tier → prevent regression. Don't reach for metaphysical solutions right away; first run am start -W, list out the initialization in Application, and cutting half of it means you've already won half the battle.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Android has quite a few practical details, and the author has clearly stepped into the pitfalls — thanks for organizing and sharing.