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.
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()
}
}
}
}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-profileWhen 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.
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.