Shipping Faster Cold Starts with Baseline Profiles in Android Studio
Baseline Profiles let ART pre-compile your app's hot paths at install time, fixing slow first runs. Here's how to generate one in Android Studio and measure the actual gain.
14 Sept 2025, 13:05 UTC

The first-run problem nobody escapes
Your app feels snappy on your development machine after a few runs, but a user installing it fresh from the Play Store sees a sluggish first launch and a janky first scroll. That's not your imagination. On Android, the runtime (ART) compiles bytecode to native code incrementally as your app runs — a process called just-in-time (JIT) compilation. During those first minutes, hot code paths are still being interpreted or partially compiled, and the user pays for it in latency.
Baseline Profiles exist to fix exactly this. A Baseline Profile is a list of classes and methods that ships inside your APK or App Bundle. At install time, ART uses it to ahead-of-time (AOT) compile the code paths that matter — startup, the first screen, common navigation — so the first run behaves more like the tenth run. Android Studio has first-class tooling to generate these profiles, and this post walks through the practical decision of adopting them.
How the generation pipeline works
The flow, in Android Studio Hedgehog and later with a recent Android Gradle Plugin (AGP), looks like this:
- Add a Baseline Profile Generator module via File → New → New Module. This sets up a module wired to the Baseline Profile Gradle plugin and the Macrobenchmark library.
- Write a Macrobenchmark-driven UI test that exercises a real user journey — typically cold startup plus a scroll or a navigation to a key screen.
- Run the generator task (commonly something like
generateReleaseBaselineProfile, though exact task names vary by AGP version) against a connected device or emulator. - The tool produces a
baseline-prof.txtfile that gets packaged into your release artifact. When the Play Store distributes your app, it can also aggregate profiles across users (cloud profiles) to refine compilation further.
The key insight is that the profile is captured from real execution, not guessed. The generator literally runs your app and records which methods got hot.
A worked example: startup plus a RecyclerView scroll
Here's the shape of a generator test. It lives in the baseline profile module and runs on a device or emulator — you need instrumentation permissions, which the plugin handles via the generated test runner setup:
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() = rule.collect(
packageName = "com.example.myapp"
) {
pressHome()
startActivityAndWait()
// Exercise a real journey: scroll the main list
device.findObject(By.scrollable(true))
?.fling(Direction.DOWN)
device.wait(Until.hasObject(By.text("Settings")), 5_000)
}
}Two things matter here. First, pressHome() before startActivityAndWait() ensures a true cold start rather than resuming a warm process. Second, the scroll and wait steps capture the code paths for your most common post-startup interaction — that's what gets pre-compiled, not just the launcher activity.
Run the generator from Android Studio's run configurations or from the command line in your project root (no special host permissions needed, but the target device must allow instrumentation):
./gradlew :app:generateReleaseBaselineProfileThen rebuild your release AAB and verify the profile actually shipped. Unzip the artifact (or use Android Studio's APK Analyzer) and look for the profile under assets/dexopt/baseline.prof. If it's missing, the plugin isn't wired to the variant you built — a common gotcha when flavors are involved.
Prove it worked before you celebrate
Don't quote a percentage improvement you read in someone else's blog post — gains vary enormously by app. Measure your own build with Macrobenchmark's StartupTimingMetric, comparing compilation modes on the same APK:
@Test
fun startupWithProfile() = benchmarkRule.measureRepeated(
packageName = "com.example.myapp",
metrics = listOf(StartupTimingMetric()),
compilationMode = CompilationMode.Partial(
BaselineProfileMode.Require
),
iterations = 10,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}Run the same test with CompilationMode.None() as your baseline. Compare median time-to-initial-display between the two runs. CompilationMode.None simulates a device with no profile; Partial with the profile required simulates what an installed user gets. The delta is your honest answer, and it's usually largest for cold start and first-frame rendering.
The trade-off you sign up for
Baseline Profiles are not free. The profile reflects your code as it was when generated. Refactor your startup path or add a new critical screen, and the stale profile silently helps less — it won't break anything, but you're shipping dead weight and missing the new hot paths. That means regeneration belongs in CI, ideally run on a schedule or before each release, which adds build time and a device/emulator dependency to your pipeline.
Also calibrate expectations: profiles shine on cold-start latency and first-run jank. They do little for steady-state performance of an app that's been running for an hour, because JIT has long since caught up. If your performance problem is a slow network call or an unoptimized layout pass, fix that first.
One more caveat: plugin IDs, task names, and AGP integration details have shifted across releases. Before following any tutorial (including this one), check that your Baseline Profile plugin version matches your project's AGP version, and confirm the exact task names in your build with ./gradlew tasks --all.
Where to start this week
If your app has measurable cold-start complaints, the cheapest experiment is: add the generator module, write one test covering cold start plus your single most common interaction, generate the profile, and run the two-mode Macrobenchmark comparison. That takes an afternoon and gives you real numbers for your own build. If the median startup delta justifies the CI maintenance, wire regeneration into your release pipeline and expand the journeys you capture. If it doesn't, you've lost a few hours and gained a benchmark harness you'll reuse anyway.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.