There is a performance optimisation available to essentially every Android app that requires no architectural change, takes about half a day to set up, and produces a measurable cold start improvement concentrated exactly on the devices where you need it most.
Most codebases I look at do not have it.
What the problem actually is
When Android installs your app, most of your code ships as DEX bytecode and gets interpreted or JIT-compiled at runtime. The ART runtime eventually profiles what runs hot and compiles those paths ahead of time — but "eventually" means after several sessions on the user's device.
So the first run, which is the run that decides whether someone keeps your app, is the slowest one it will ever be.
A baseline profile is a list of classes and methods that get compiled ahead of time at install. You ship it with the app, and the expensive first-run penalty largely disappears.
The improvement is disproportionately large on slower hardware, because interpretation cost scales with how slow the CPU is. On a flagship you might see a modest gain. On the three-year-old mid-tier device that represents the median user in most markets, the difference is substantial — and that is precisely the device that is failing you today.
Setting it up
Add the baseline profile Gradle plugin and a benchmark module.
// build.gradle.kts (app)
plugins {
id("androidx.baselineprofile")
}
dependencies {
baselineProfile(project(":baselineprofile"))
}
Then write a generator that drives the journeys you care about. The critical judgement is which journeys — profile the paths users actually take on first launch, not every screen in the app.
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule val rule = BaselineProfileRule()
@Test
fun generate() = rule.collect(packageName = "com.example.app") {
pressHome()
startActivityAndWait()
// Cold start path
device.wait(Until.hasObject(By.res("home_feed")), 5_000)
// Primary scroll surface
device.findObject(By.res("home_feed")).also {
it.setGestureMargin(device.displayWidth / 5)
it.fling(Direction.DOWN)
it.fling(Direction.UP)
}
// The one journey that drives revenue
device.findObject(By.res("cta_checkout")).click()
device.wait(Until.hasObject(By.res("checkout_form")), 5_000)
}
}
Generate with ./gradlew :app:generateReleaseBaselineProfile. The output lands in src/release/generated/baselineProfiles/ and ships in the release build.
Verifying it — the step that gets skipped
Do not assume the improvement. Measure it, on a device that resembles what your users hold.
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule val rule = MacrobenchmarkRule()
@Test fun startupNoProfile() = startup(CompilationMode.None())
@Test fun startupWithProfile() = startup(
CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require)
)
private fun startup(mode: CompilationMode) = rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
compilationMode = mode,
iterations = 10,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
}
Run both on a mid-tier physical device. An emulator on your development machine will produce numbers that tell you nothing about the population you are shipping to — it is running on your CPU with your SSD.
Record the numbers. timeToInitialDisplay is the honest metric; if you have meaningful async content loading after first frame, track timeToFullDisplay too and report both.
Wiring it into CI
The failure mode is that a baseline profile gets generated once, someone adds a new startup dependency six months later, and the profile silently stops covering the hot path.
Regenerate on a schedule and on any change to startup code, and assert on the benchmark result:
- name: Generate baseline profile
run: ./gradlew :app:generateReleaseBaselineProfile
- name: Startup benchmark
run: ./gradlew :baselineprofile:connectedReleaseAndroidTest
- name: Fail on regression
run: python scripts/check_startup_budget.py --max-ms 1400
A numeric budget matters here. "Startup should be fast" is not a budget. timeToInitialDisplay under a specific figure on a named device is.
Where this sits in the bigger picture
Baseline profiles are worth doing on their own merits, but their real value as a signal is diagnostic.
When I am assessing whether an Android team operates apps or merely builds them, this is one of the questions I ask, along with their ANR rate and how they handled their last target API migration. Baseline profiles are cheap, well documented, and measurably effective — a team that ships production Android and has not set them up usually has not been measuring performance on real user hardware at all.
Which is the actual problem. The profile is a fix. The absence of one is a symptom.
I wrote the full buyer-side guide — scope, cost ranges, native versus cross-platform, Play Store compliance as an engineering workstream, and the questions that expose a weak vendor: Android App Development Services: What to Evaluate in 2026.
TechCirkle does mobile app development work if you want a second opinion on an Android build.
Frequently Asked Questions
What is an Android baseline profile?
A list of classes and methods compiled ahead of time at install, so the expensive first-run interpretation and JIT penalty largely disappears. It ships with the release build and improves cold start and early scroll performance.
How much does a baseline profile improve cold start?
Enough to matter, and disproportionately on slower hardware, because interpretation cost scales with CPU speed. Gains on flagships are modest; gains on mid-tier devices — where most users are — are substantially larger.
Which user journeys should a baseline profile cover?
The paths users actually take on first launch: the cold start route to your main screen, your primary scroll surface, and the journey that drives revenue. Profiling every screen dilutes the benefit.
How do you verify a baseline profile works?
With Macrobenchmark, comparing CompilationMode.None() against CompilationMode.Partial with the profile required, running cold-start iterations on a physical mid-tier device. Emulator numbers reflect your development machine, not your users.
Why do baseline profiles need CI integration?
Because a profile generated once goes stale. Adding a startup dependency months later can leave the hot path uncovered with no visible symptom. Regenerate on schedule and assert against a numeric startup budget.
What does a missing baseline profile indicate about a team?
Usually that nobody is measuring performance on real user hardware. The profile itself is a cheap fix; its absence is a symptom of a wider gap in how the app is operated.


Top comments (0)