ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-06-23Advanced

Only Slow Right After Install — Cutting Android Cold-Start Time with Baseline Profiles, Measured

Why an Android app stutters on launch only right after install or update, explained through JIT and Cloud Profiles, plus a measured walkthrough of cutting cold-start time with Baseline Profiles — from building a Macrobenchmark harness to staged rollout, from an indie developer's perspective.

Android15Baseline ProfileMacrobenchmarkPerformance2Cold Start

Premium Article

On a fresh device, the very first tap into my wallpaper app left half a beat of silence before the home grid appeared. Every launch after that was instant. Slow only in release builds, and only when the install was brand new — a symptom that nagged at me quietly for about half a year.

When you run the same app for years as an indie developer, your own device has a fully warmed profile, so you never feel this slowness yourself. I only caught it when a review said "heavy the first time." The cause lived in the timing of JIT and Cloud Profiles, and a Baseline Profile shrank it in a way I could actually measure. Here is the path I took, starting with how to measure before you change anything.

Why it's slow only when the install is new

Right after installation, most of an Android app's code runs interpreted or through the JIT (just-in-time compiler). As hot paths get exercised, ART gradually compiles them ahead of time, which is why a device you've used for a while launches faster. That is the real reason "it doesn't stutter after the first time."

Google Play has a mechanism called Cloud Profiles that aggregates execution profiles from many users and ships a somewhat warm state to new installs. But it only kicks in once enough usage has accumulated after release. A new install right after launch — or an app update, where the profile is reset — sees little of that benefit.

A Baseline Profile lets you enumerate the hot paths for startup and your busiest screens ahead of time, bundle them into the APK / AAB, and have them AOT-compiled at install. Instead of waiting for Cloud Profiles to mature, you ship a warm state from the very first launch. My symptom was exactly this "cold only at the start" state, so it lined up well.

Build something measurable first — the Macrobenchmark module

Before touching any optimization, get to a state you can reproduce in numbers. Skip this and add only the profile, and you'll never know whether it helped or you imagined it. The first time I added a Baseline Profile, I could only describe it by feel, which made for an unconvincing note to myself.

For measurement I use Macrobenchmark. Separate from the app itself, you add a dedicated com.android.test module.

// build.gradle.kts of the :macrobenchmark module
plugins {
    id("com.android.test")
    id("org.jetbrains.kotlin.android")
}
 
android {
    namespace = "com.example.wallpaper.macrobenchmark"
    compileSdk = 35
    defaultConfig {
        minSdk = 24
        targetSdk = 35
        // Runs on a physical device against a release-like build
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }
    targetProjectPath = ":app"
    experimentalProperties["android.experimental.self-instrumenting"] = true
}
 
dependencies {
    implementation("androidx.test.ext:junit:1.2.1")
    implementation("androidx.test.uiautomator:uiautomator:2.3.0")
    implementation("androidx.benchmark:benchmark-macro-junit4:1.3.3")
}

On the app side, make it profileable so the benchmark can launch and measure it. Use a release-like benchmark build type rather than a debug build, and the numbers reflect reality much better.

<!-- app/src/main/AndroidManifest.xml -->
<application ...>
    <profileable
        android:shell="true"
        tools:targetApi="29" />
</application>

The test that measures startup itself looks like this. StartupTimingMetric reports the time from tap to first frame (Time To Initial Display) with a median.

// StartupBenchmark.kt
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
    @get:Rule val rule = MacrobenchmarkRule()
 
    private fun measure(mode: CompilationMode) = rule.measureRepeated(
        packageName = "com.example.wallpaper",
        metrics = listOf(StartupTimingMetric()),
        iterations = 15,             // higher count so the spread is visible
        startupMode = StartupMode.COLD,
        compilationMode = mode,
    ) {
        pressHome()
        startActivityAndWait()
    }
 
    // Bare state with no profile (the worst-case baseline)
    @Test fun coldStartupNone() = measure(CompilationMode.None())
 
    // With the Baseline Profile required
    @Test fun coldStartupBaselineProfile() =
        measure(CompilationMode.Partial(BaselineProfileMode.Require))
}

I set iterations to 15 because cold start has wide variance depending on device state. With one or two runs the median won't settle, and you'll misjudge the improvement.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Diagnose why launch is slow only right after install using JIT and Cloud Profiles, so you can decide whether Baseline Profiles will help your own app
Measure startup time as reproducible before/after numbers using a Macrobenchmark module and CompilationMode
Bundle a generated Baseline Profile into your AAB and roll it out safely while watching startup metrics through staged release
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

App Dev2026-06-22
Letting Users Switch the App's Language Without Touching Their Device Language — Notes on Android Per-App Language
How to implement in-app language switching on Android with AppCompatDelegate.setApplicationLocales and locales_config, plus the gotchas around pre-API-33 compatibility, activity recreation, and AdMob, from an implementation point of view.
App Dev2026-06-21
A Few Low-Density Phones Lost Their Bundled Wallpaper — The drawable vs nodpi Boundary in Play's Density Splits
App Bundle density splits will happily split images that should never be split, dropping a static resource on one density bucket only. Here is how I reproduced it with bundletool and fixed it by moving to drawable-nodpi or disabling density split — with the decision criteria.
App Dev2026-06-18
Vetting AI Studio's Native Android Code Before It Reaches Your Live App
AI Studio's native Android vibe coding produces working screens at startling speed. But before it goes into a live app, it needs its own vetting. Here is a pre-merge review design for generated Kotlin.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →