跪拜 Guibai
← Back to the summary

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:

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:

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:

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 SplashActivityMainActivity 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:

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.

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

AI向善

Android has quite a few practical details, and the author has clearly stepped into the pitfalls — thanks for organizing and sharing.