SYSONLINE
SDK36 · MIN 24
KOTLIN2.0
UTC22:22:17
UP00:01
MOD · ARTICLE · PERFORMANCES/N · AX-BASELIPUBLISHED
All Articles
Performance · Updated 12 MIN

Baseline Profiles and R8: Advanced Android Performance Tuning

Baseline Profiles compile critical code paths before first launch, and R8 shrinks and optimizes the rest. What each does, how to set them up, what to delete.

By Rocky Elsalaymeh · Founder & Principal Consultant, Strategia-X

What do Baseline Profiles and R8 actually do?

Most performance work happens inside your code: fewer recompositions, cached responses, lazy images. Two tools work below it, at compile and packaging time, and most teams never look at them after the project template sets them up. Baseline Profiles tell the Android Runtime which code paths to compile ahead of time. Without one, in Android's words, "all app code is either JIT-compiled in memory after being interpreted, or written to an odex file in the background when the device is idle", and users "have a suboptimal experience from the first time they run it until new code paths are optimized". With one, "Baseline Profiles improve code execution speed by about 30% from the first launch by avoiding interpretation and just-in-time (JIT) compilation steps for included code paths". R8 is the app optimizer. It "streamlines your app by removing unused code and resources, rewriting code to optimize runtime performance, and more". This article was rewritten in October 2026 and three things in the earlier version did not survive the check. It said Google reports a 30 to 40% improvement in time to initial display; the documented figure is about 30% in code execution speed, which is a different measurement. It said R8 removes 40 to 60% of unused code, a number with no source. And it presented a results table from a "Pixel 7" that was never measured by anyone. All three are gone. Versions read on 11 October 2026: Macrobenchmark 1.5.0, Profile Installer 1.4.1, Android Gradle plugin 9.4.0.

Generating Baseline Profiles with Macrobenchmark

A profile is produced by a test that drives your app through the journeys you care about while the Macrobenchmark library records what ran. The setup guide gives two routes. The short one is the Baseline Profile Generator module template, "available starting in Android Studio Iguana and Android Gradle Plugin (AGP) 8.2". It creates the module, the generator and a benchmark. The manual one is three steps: a com.android.test module, the androidx.baselineprofile plugin, and targetProjectPath pointing at the app. The sample below is that route. Two facts save an afternoon of confusion: - The device. The generator needs a rooted device or emulator, with one exception: "you can generate the Baseline Profile on devices running Android 13 (API 33) and higher without root access". - The build type. "To keep installation turnaround during development low, Baseline Profiles are only installed for release builds." A debug build will never show the improvement. On AGP 9 the module does not apply the kotlin-android plugin. The earlier version of this sample did, and AGP 9 rejects it; the reason is in the version catalogs and convention plugins guide.
kotlin
// :baseline-profile/build.gradle.kts
plugins {
    id("com.android.test")
    id("androidx.baselineprofile")
}

android {
    namespace = "com.example.baselineprofile"
    // The module the profile is generated for
    targetProjectPath = ":app"
}

dependencies {
    implementation("androidx.benchmark:benchmark-macro-junit4:1.5.0")
}

// :app/build.gradle.kts
plugins {
    id("com.android.application")
    id("androidx.baselineprofile")
}

dependencies {
    implementation("androidx.profileinstaller:profileinstaller:1.4.1")
    baselineProfile(project(":baseline-profile"))
}

baselineProfile {
    // Regenerate the profile for each release assembly
    automaticGenerationDuringBuild = true
    // Store it under src/<variant>/generated/baselineProfiles
    saveInSrc = true
}

Writing Profile Generator Rules

The generator is a JUnit test built on BaselineProfileRule. Each collect call runs the block repeatedly until the profile is stable. Per the reference, maxIterations defaults to 15 and stableIterations to 3. Split startup from everything else. Android's guidance: "Make sure that any tests related to app startup are in a rule block with includeInStartupProfile set to true". That flag feeds a second artifact, the Startup Profile. Startup profiles "are used at compile time to optimize DEX layout for faster startup times, rather than for on-device optimization", and the overview says plainly: "We recommend using both Baseline Profiles and startup profiles to fully optimize app startup." One rule surprises people who know R8: "When capturing a Baseline Profile with a test, your app must not be obfuscated." The release build still is. R8 rewrites the profile rules to match the obfuscated names when it builds the release, and "The Baseline Profile Gradle Plugin automatically handles this configuration for you." The earlier version of this article said the output is app/src/main/baseline-prof.txt. With the plugin it is src/<variant>/generated/baselineProfiles/baseline-prof.txt.
kotlin
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun startup() = rule.collect(
        packageName = "com.example.app",
        // Startup code also goes into the Startup Profile
        includeInStartupProfile = true,
    ) {
        pressHome()
        startActivityAndWait()
    }

    @Test
    fun criticalJourney() = rule.collect(
        packageName = "com.example.app",
    ) {
        startActivityAndWait()
        // Drive the journeys that matter here: open a detail
        // screen, scroll the main list, run a search.
    }
}

// Generate from the command line:
// ./gradlew :app:generateBaselineProfile

R8: What an Old Rules File Gets Wrong

R8 is not something you tune with a long rules file any more. Most of what older files contain is now a default, or is ignored. - Full mode is the default. "R8 full mode has been the default since Android Gradle Plugin (AGP) 8.0" (Android). - You still have to switch optimization on. The earlier version of this article said R8 runs in release builds by default. Android's wording is an instruction, not a description: "You should always enable optimization for your app's release build". - The switch moved. From AGP 9.3 it is an optimization block, and "The updated DSL enables both code and resource optimization." isMinifyEnabled and isShrinkResources are documented as the legacy DSL for lower versions. The same page records that the non-optimizing default file, proguard-android.txt, is no longer supported. - Some options do nothing. The global options page lists what "R8 ignores the following options without reporting a warning", and the list includes -optimizationpasses and -mergeinterfacesaggressively. This article used to recommend both under the heading of aggressive optimization. The advice from Android: "Because they have no effect on R8 output, remove them from your proguard-rules.pro file to keep your configuration easier to understand and maintain." - Two more are already on. For -repackageclasses: "Since Android Gradle Plugin (AGP) 9.1, this is the default configuration for apps." For -allowaccessmodification: "Since Android Gradle Plugin (AGP) 8.2, this is the default configuration if you enable full optimization with R8". That leaves keep rules, and the rule for those is to write fewer. "Well-defined keep rules are as specific as possible" (Android). The old sample kept every class in androidx.compose.runtime, which is the opposite, and repeated rules for Retrofit, Room and kotlinx.serialization that those libraries are expected to bring themselves: "Libraries designed to perform well on Android should avoid reflection where possible and embed consumer keep rules when necessary".
properties
// app/build.gradle.kts, AGP 9.3 and higher
android {
    buildTypes {
        release {
            optimization {
                enable = true // Code and resource optimization
            }
        }
    }
}

# proguard-rules.pro: what to delete from an old file
#
# -optimizationpasses 5            ignored by R8
# -mergeinterfacesaggressively     ignored by R8
# -repackageclasses ''             default for apps since AGP 9.1
# -allowaccessmodification         default since AGP 8.2
# -keep class androidx.compose.runtime.** { *; }   package-wide keep
#
# What is left is specific to your own code, for example a class
# you reach by reflection:
-keepclassmembers class com.example.MyClass {
    void someSpecificMethod();
}

Measuring Impact: Before and After

Measure on your app. The guide says so directly: "After you generate the Baseline Profile, benchmark it using a physical device to measure the speed improvements." The benchmark below runs the same cold start twice, once with no compilation and once with the profile required. StartupTimingMetric reports two numbers (reference): timeToInitialDisplayMs, the "Time from the system receiving a launch intent to rendering the first frame of the destination Activity", and timeToFullDisplayMs, which is only reported if the app calls reportFullyDrawn(). The mode matters. BaselineProfileMode.Require is documented as "Require the BaselineProfile methods/classes from the target app to be pre-compiled", where UseIfAvailable only includes them when a profile and the ProfileInstaller library are both present (reference). A benchmark that claims to measure the profile should require it. What the numbers mean against the thresholds Google Play applies is covered in the startup optimization guide, and running the benchmark on every change belongs in CI. One sourced figure, for calibration and not as a promise: Android credits the R8 rewriting of profile rules that arrived in AGP 8.2 with an increase in profile coverage of methods of about 30% and in app performance of about 15%.
kotlin
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {

    @get:Rule
    val rule = MacrobenchmarkRule()

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

    @Test
    fun startupWithBaselineProfile() = startup(
        CompilationMode.Partial(
            baselineProfileMode = BaselineProfileMode.Require,
            warmupIterations = 0,
        )
    )

    private fun startup(compilationMode: CompilationMode) {
        rule.measureRepeated(
            packageName = "com.example.app",
            metrics = listOf(StartupTimingMetric()),
            compilationMode = compilationMode,
            startupMode = StartupMode.COLD,
            iterations = 10,
        ) {
            pressHome()
            startActivityAndWait()
        }
    }
}
MOD · TAKEAWAYS6 POINTSSUMMARY

Key Takeaways

  1. 1Baseline Profiles improve code execution speed by about 30% from the first launch, according to Android. Measure your own app before quoting a number.
  2. 2Profiles are only installed for release builds, and the generator needs a rooted device or one on API 33 or higher.
  3. 3Mark startup tests with includeInStartupProfile so the same run also produces a Startup Profile.
  4. 4R8 full mode has been the default since AGP 8.0. From AGP 9.3, optimization is enabled with the optimization block.
  5. 5Delete -optimizationpasses and -mergeinterfacesaggressively from old rules files. R8 ignores them without a warning.
  6. 6Keep rules should be as specific as possible. A package-wide keep undoes the optimization you turned on.
MOD · FAQ3 ENTRIESANSWERED

Frequently Asked

Do I have to regenerate the Baseline Profile for every release?

It should match the code you ship. Setting automaticGenerationDuringBuild to true triggers the generation of a new profile for each release assembly, at the cost of a device or emulator in the build. The alternative is to run the generateBaselineProfile task when critical journeys change and commit the result with saveInSrc.

Can I generate a Baseline Profile from an obfuscated build?

No. Android states that when capturing a Baseline Profile with a test, your app must not be obfuscated, so the rules match real method signatures. The release build stays obfuscated: R8 rewrites the profile rules to the obfuscated names, and the Baseline Profile Gradle plugin sets up the non-minified variant for you.

Which ProGuard options does R8 ignore?

Android lists the options R8 ignores without reporting a warning, including -optimizationpasses, -mergeinterfacesaggressively, -overloadaggressively, -dontpreverify and -verbose. They have no effect on the output, so remove them. Two other common ones, -repackageclasses and -allowaccessmodification, are already the default on current Android Gradle plugin versions.

MOD · BUILD · NEXTS/N · AX-CTA-0001READY

Ready to architect your next Android app?

ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.

ANDROID-ARCHITECT · CONSOLE
S/N · AX-1A-00001
STRATEGIA-X · ENGINEERED · IN · CALIFORNIA