Crushing Android App Startup Latency: Baseline Profiles & DEX Optimization

Crushing Android App Startup Latency: Baseline Profiles & DEX Optimization

First impressions matter. In Android app development, slow startup times lead directly to user churn. If your app takes longer than two seconds to load, users will close it and move to a competitor.

But since Android uses Ahead-Of-Time (AOT) and Just-In-Time (JIT) compilers, optimizing startup is complex. On first launch, the OS must interpret your app’s bytecode, causing CPU spikes and laggy frames.

Enter Baseline Profiles. By pre-compiling critical code paths, you can speed up app startup by up to 30% and eliminate scrolling stutter.

In this guide, we will implement Baseline Profiles using the modern Jetpack Macrobenchmark tool.

What is a Baseline Profile?

A Baseline Profile is a list of classes and methods that your app uses during critical flows (like startup, list scrolling, or page transitions).

When your app is installed, the Android runtime (ART) uses this profile to pre-compile that specific code into native machine code. This eliminates the need for the CPU to interpret bytecode during initial runs, providing immediate performance gains.

Without Baseline Profile:
[ Launch ] ---> [ Interpret Bytecode (Slow, High CPU) ] ---> [ App Ready ]

With Baseline Profile:
[ Launch ] ---> [ Execute Pre-compiled Machine Code (Fast) ] ---> [ App Ready ]

Step 1: Create a Benchmark Module

To generate a profile, you need a dedicated test module. In Android Studio, go to File > New > New Module and select Baseline Profile Generator. This creates a module pre-configured with the Macrobenchmark library.

Step 2: Write the Generator Test

Inside the new module, write a test that simulates the user’s startup journey:

import androidx.benchmark.macro.junit4.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 baselineRule = BaselineProfileRule()

    @Test
    fun generateStartupProfile() {
        baselineRule.collect(
            packageName = "com.portfolio.app",
            // Profile collection triggers multiple launches
            maxIterations = 3
        ) {
            // Define actions to profile
            pressHome()
            startActivityAndWait()
            
            // Optional: simulate scroll interaction to compile list views
            device.waitForIdle()
        }
    }
}

Step 3: Run and Package the Profile

Run the generator test on a rooted emulator or physical device. The test output will produce a baseline-prof.txt file.

The Gradle plugin automatically copies this file into your app’s src/main/ folder. When you build your release App Bundle (AAB), Gradle packages the profile into the build. Google Play then distributes the profile to users during installation.

Step 4: Verify the Performance Gain

Write a macrobenchmark test to verify the optimization results:

@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun startupCompilationNone() = startup(CompilationMode.None)

    @Test
    fun startupCompilationBaselineProfiles() = startup(CompilationMode.Partial())

    private fun startup(compilationMode: CompilationMode) {
        benchmarkRule.measureRepeated(
            packageName = "com.portfolio.app",
            metrics = listOf(StartupTimingMetric()),
            compilationMode = compilationMode,
            iterations = 5
        ) {
            pressHome()
            startActivityAndWait()
        }
    }
}

The results will show a clear side-by-side comparison, highlighting the millisecond startup latency savings achieved with Baseline Profiles.