Android Baseline Profiles: Optimizing App Startup and Runtime Performance

Android Baseline Profiles for Startup Performance

Android Baseline Profiles startup optimization is one of the most impactful performance improvements you can make to any Android application. Baseline Profiles provide AOT (Ahead-of-Time) compilation hints to the Android Runtime (ART), telling it which code paths are critical and should be pre-compiled at install time rather than JIT-compiled at runtime. In practice, this reduces cold start time by 30-40% and eliminates jank during initial interactions.

This guide covers creating, testing, and distributing Baseline Profiles for production Android applications, from Macrobenchmark setup to profile generation, custom journey coverage, and CI/CD integration. Moreover, you will learn how to measure the actual impact on startup metrics and optimize both cold start and warm start scenarios.

Understanding the ART Compilation Pipeline

When you install an Android app, ART uses a combination of interpretation, JIT compilation, and profile-guided AOT compilation to execute your code. Without Baseline Profiles, the app starts with interpreted code (slow), JIT-compiles hot methods over time, and eventually AOT-compiles based on usage profiles. This means the first several launches are significantly slower than later ones.

Furthermore, Baseline Profiles short-circuit this process by providing compilation hints at install time. Google Play processes your profile and compiles the specified methods before the user first launches the app. As a result, the very first launch benefits from pre-compiled native code for critical paths.

Android app performance optimization
Baseline Profiles provide AOT compilation hints for faster startup

Setting Up Macrobenchmark for Baseline Profiles

// Add benchmark module to your project
// settings.gradle.kts
include(":app")
include(":benchmark")

// benchmark/build.gradle.kts
plugins {
    id("com.android.test")
    id("org.jetbrains.kotlin.android")
    id("androidx.baselineprofile")
}

android {
    namespace = "com.myapp.benchmark"
    compileSdk = 35

    defaultConfig {
        minSdk = 28
        targetSdk = 35
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    targetProjectPath = ":app"
}

baselineProfile {
    useConnectedDevices = true
}

dependencies {
    implementation("androidx.test.ext:junit:1.2.1")
    implementation("androidx.test.espresso:espresso-core:3.6.1")
    implementation("androidx.test.uiautomator:uiautomator:2.3.0")
    implementation("androidx.benchmark:benchmark-macro-junit4:1.3.3")
}

Android Baseline Profiles: Generating the Profile

// benchmark/src/main/kotlin/BaselineProfileGenerator.kt
package com.myapp.benchmark

import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {

    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun generateBaselineProfile() {
        rule.collect(
            packageName = "com.myapp",
            stableIterations = 3,
            maxIterations = 10,
            includeInStartupProfile = true
        ) {
            // Cold start — app launch
            pressHome()
            startActivityAndWait()

            // Critical user journey 1: Browse products
            device.findObject(By.res("product_list")).wait(Until.hasObject(
                By.res("product_card")), 5000)

            // Scroll the product list
            val list = device.findObject(By.res("product_list"))
            list.scroll(Direction.DOWN, 1.0f)
            list.scroll(Direction.DOWN, 1.0f)
            device.waitForIdle()

            // Critical user journey 2: View product detail
            device.findObject(By.res("product_card")).click()
            device.findObject(By.res("product_detail")).wait(
                Until.hasObject(By.res("add_to_cart")), 5000)

            // Critical user journey 3: Add to cart
            device.findObject(By.res("add_to_cart")).click()
            device.waitForIdle()

            // Critical user journey 4: Open cart
            device.findObject(By.res("cart_icon")).click()
            device.findObject(By.res("cart_list")).wait(
                Until.hasObject(By.res("cart_item")), 5000)

            // Critical user journey 5: Search
            device.pressBack()
            device.findObject(By.res("search_icon")).click()
            device.findObject(By.res("search_input")).text = "shoes"
            device.waitForIdle()
            Thread.sleep(2000)  // Wait for search results
        }
    }
}

Startup Benchmarks: Measuring the Impact

Therefore, measuring the actual impact of Baseline Profiles requires systematic benchmarking with Macrobenchmark. Run benchmarks with and without profiles to quantify the improvement.

// benchmark/src/main/kotlin/StartupBenchmark.kt
@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupBenchmark {

    @get:Rule
    val rule = MacrobenchmarkRule()

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

    @Test
    fun startupCompilationPartial() = startup(
        CompilationMode.Partial(
            baselineProfileMode = BaselineProfileMode.Require
        )
    )

    @Test
    fun startupCompilationFull() = startup(CompilationMode.Full())

    private fun startup(compilationMode: CompilationMode) {
        rule.measureRepeated(
            packageName = "com.myapp",
            metrics = listOf(
                StartupTimingMetric(),
                TraceSectionMetric("ActivityCreate"),
                TraceSectionMetric("FirstDraw"),
                TraceSectionMetric("DataLoaded"),
            ),
            iterations = 10,
            compilationMode = compilationMode,
            startupMode = StartupMode.COLD
        ) {
            pressHome()
            startActivityAndWait()

            // Wait for meaningful content to render
            device.findObject(By.res("product_list")).wait(
                Until.hasObject(By.res("product_card")), 10000
            )
        }
    }

    @Test
    fun scrollPerformanceWithProfile() {
        rule.measureRepeated(
            packageName = "com.myapp",
            metrics = listOf(
                FrameTimingMetric(),
            ),
            iterations = 5,
            compilationMode = CompilationMode.Partial(
                baselineProfileMode = BaselineProfileMode.Require
            ),
            startupMode = StartupMode.WARM
        ) {
            startActivityAndWait()

            val list = device.findObject(By.res("product_list"))
            list.wait(Until.hasObject(By.res("product_card")), 5000)

            // Measure scroll performance
            repeat(5) {
                list.scroll(Direction.DOWN, 1.0f)
                device.waitForIdle()
            }
        }
    }
}
Mobile app benchmark and testing
Comparing startup times with and without Baseline Profiles

CI/CD Integration

Additionally, generating Baseline Profiles should be part of your CI/CD pipeline to ensure they stay current as your code changes. Stale profiles can actually hurt performance if they reference removed code paths.

# .github/workflows/baseline-profile.yml
name: Generate Baseline Profile
on:
  push:
    branches: [main]

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      - name: Enable KVM for emulator
        run: |
          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666"' | sudo tee /etc/udev/rules.d/99-kvm.rules
          sudo udevadm control --reload-rules
          sudo udevadm trigger --name-match=kvm

      - name: Generate Baseline Profile
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          arch: x86_64
          profile: pixel_6
          script: ./gradlew :benchmark:pixel6Api34BenchmarkAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.myapp.benchmark.BaselineProfileGenerator

      - name: Copy profile to app module
        run: cp benchmark/build/outputs/managed_device_android_test_additional_output/pixel6Api34/BaselineProfileGenerator_generateBaselineProfile-baseline-prof.txt app/src/main/baseline-prof.txt

      - name: Create PR with updated profile
        uses: peter-evans/create-pull-request@v6
        with:
          title: "Update Baseline Profile"
          commit-message: "chore: regenerate baseline profile"
          branch: update-baseline-profile

When NOT to Use Baseline Profiles

Baseline Profiles add build complexity and require a physical device or emulator for generation. For apps with extremely simple UIs (single-screen utility apps, calculator-type apps), the startup improvement may be negligible — ART’s JIT compiler handles simple code paths efficiently. As a result, the effort to maintain benchmarks and profile generation may not be worthwhile for apps that already start in under 500ms.

Additionally, Baseline Profiles only help with cold and warm starts. If your performance issues are in rendering (dropped frames during scrolling, slow animations), Baseline Profiles alone will not fix those — you need to address the rendering pipeline directly with lazy loading, view recycling, or Compose optimization.

Android development tools and optimization
Evaluating performance optimization strategies for your app

Key Takeaways

Android Baseline Profiles startup optimization delivers measurable cold start improvements of 30-40% with relatively low implementation effort. The Macrobenchmark library provides reliable measurement, and profile generation can be automated in CI/CD. Furthermore, covering critical user journeys in your profile ensures that not just startup but also first-interaction performance is optimized.

Start by adding the benchmark module and generating a basic startup profile — even without custom journey coverage, you will see meaningful improvements. For detailed reference, consult the Android Baseline Profiles documentation and Macrobenchmark guide. Our posts on Jetpack Compose performance and Kotlin Multiplatform with Compose provide complementary Android development optimization strategies.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top