Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use this skill to ensure Jetpack Compose performance numbers reflect production reality by measuring against a release variant with R8 enabled, Live Literals disabled, and Compose Compiler reports read from the release output directory. Covers why debug builds lie (interpreted Compose runtime, JIT warmup, Live Literals constant-getters), how to set up a release-with-symbols measurement build, and how to wire Macrobenchmark, Compose Compiler reports, Layout Inspector, simpleperf, and Android Stud
.claude/skills/skydoves-testing-compose-in-release-mode/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 148% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 175% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 161% | 0% |
Compose ships unbundled — its UI runtime is loaded from app code, runs interpreted in debug, and the Live Literals plugin turns every constant into a getter. Debug recomposition counts, debug startup numbers, and debug compiler reports are not what users experience. This skill teaches Claude how to set up a release-with-symbols build for honest measurement before any perf claim is made.
CompilationMode.None.../../build/configuring-r8-for-compose/SKILL.md.../generating-baseline-profiles/SKILL.md.../../stability/diagnosing-compose-stability/SKILL.md.signingConfig signingConfigs.debug on the release type), but unsigned release builds will not install.org.jetbrains.kotlin.plugin.compose Gradle plugin.Surface these to the developer when they push back on "but my numbers feel real":
sourceInformation() calls, devirtualizes ComposerImpl, and constant-folds composable args.0.dp, "Hello", Color.Red — in a getter. The recomposer treats these as dynamic, so reports flag composables as taking unstable params even when source code only uses constants.sourceInformation strings remain, and the cost-per-frame budget is bigger than what release ships.Cited measurement: roughly 75 percent startup gain and 60 percent frame-render gain when switching debug to release with R8. Source: Ben Trengrove, "Why should you always test Compose performance in release" (Android Developers Medium).
../../build/configuring-r8-for-compose/SKILL.md for the full keep-rule story. Minimum:kotlinandroid { buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) // Use debug signing if there is no release keystore yet — the build still installs. signingConfig = signingConfigs.getByName("debug") } } }
featureFlags enum exposes only IntrinsicRemember, OptimizeNonSkippingGroups, PausableComposition, and StrongSkipping. On the legacy Compose Compiler 1.5.x extension the property was liveLiterals, marked deprecated. For measurement, build the release variant — Live Literals is off by default in release. If a separate "benchmark" build type is used (recommended — release minus signing constraints), it inherits the release defaults.../../stability/diagnosing-compose-stability/SKILL.md for how to interpret the reports.bash./gradlew :app:assembleRelease -PcomposeCompilerReports=true ls app/build/compose_compiler/ # app_release-classes.txt, app_release-composables.txt, app_release-composables.csv, app_release-module.json
CompilationMode.Partial(BaselineProfileMode.Require). The benchmark module itself stays its own variant; the target app must be release. Cross-link ../generating-baseline-profiles/SKILL.md.kotlin@RunWith(AndroidJUnit4::class) class StartupBenchmark { @get:Rule val rule = MacrobenchmarkRule() @Test fun startupRelease() = rule.measureRepeated( packageName = "com.example", metrics = listOf(StartupTimingMetric()), iterations = 10, startupMode = StartupMode.COLD, compilationMode = CompilationMode.Partial(BaselineProfileMode.Require), ) { pressHome() startActivityAndWait() } }
@TraceRecomposition. The Layout Inspector recomposition count surface is a debug-only convenience. For an authoritative count, use skydoves' @TraceRecomposition from compose-stability-analyzer against a release-with-symbols build. Cross-link ../tracing-recompositions-at-runtime/SKILL.md.simpleperf and the Android Studio Profiler can resolve native (NDK) frames in the release variant:kotlinandroid { buildTypes { release { isMinifyEnabled = true // NDK / native symbol level only — controls .so debug symbol packaging. ndk { debugSymbolLevel = "FULL" } } } }
ndk { debugSymbolLevel = "FULL" } controls NDK / native symbol packaging only; it does not affect Kotlin / Java frame readability. Kotlin frame readability comes from mapping.txt, which R8 always produces when isMinifyEnabled = true. Pair native profiling with R8 retrace (see ../../build/configuring-r8-for-compose/SKILL.md) to map obfuscated Kotlin stacks back to source lines.
bash# WRONG ./gradlew :app:assembleDebug cat app/build/compose_compiler/debug/app-composables.txt # WRONG because: debug enables Live Literals which turns every constant into a getter, so reports show false-positive unstable params and inflate non-skippable counts.
bash# RIGHT ./gradlew :app:assembleRelease -PcomposeCompilerReports=true cat app/build/compose_compiler/app_release-composables.txt
kotlin// WRONG rule.measureRepeated( packageName = "com.example", metrics = listOf(StartupTimingMetric()), iterations = 5, startupMode = StartupMode.COLD, compilationMode = CompilationMode.None, ) // WRONG because: CompilationMode.None disables AOT compilation and the target app may also be the debug variant — interpreted Compose stack plus JIT warmup makes startup numbers 3-4x worse than what users see, and any regression diff is dominated by JIT noise.
kotlin// RIGHT rule.measureRepeated( packageName = "com.example", metrics = listOf(StartupTimingMetric()), iterations = 10, startupMode = StartupMode.COLD, compilationMode = CompilationMode.Partial(BaselineProfileMode.Require), ) // And the targetPackage points to the release variant of the app under test.
kotlin// WRONG // "Layout Inspector says ProductCard recomposes 50 times per scroll, so the bug is real // and we need to mark every parameter @Stable." // WRONG because: Layout Inspector counts are sampled and approximate; Live Literals can inflate them; the count may double or halve in release. Confirm with @TraceRecomposition or Macrobenchmark FrameTimingMetric in a release build before chasing a fix.
kotlin// RIGHT // Step 1: Reproduce in release build with @TraceRecomposition on the suspect composable. // Step 2: If the release-build trace still shows the count, then diagnose with // ../../stability/diagnosing-compose-stability/SKILL.md against the release reports. // Step 3: Fix, then re-measure with FrameTimingMetric in Macrobenchmark. @TraceRecomposition(traceStates = true) @Composable fun ProductCard(product: Product) { /* ... */ }
text// WRONG // "I ran my benchmark against the debug variant — Live Literals was on, so my recomposition // counts and frame timings were inflated, and I cannot trust the report." // WRONG because: Live Literals is on in debug to support Android Studio's live edit; it wraps // constants in getters that the recomposer treats as dynamic. Stability reports and recomposition // counts from a debug build are not measurement evidence.
kotlin// RIGHT — measure release; Live Literals is off there by default in the new (Kotlin 2.0+) // Compose Compiler Gradle DSL. The featureFlags enum exposes IntrinsicRemember, // OptimizeNonSkippingGroups, PausableComposition, and StrongSkipping — there is no LiveLiterals // flag to set. Just build the release variant and read the release reports: composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") metricsDestination = layout.buildDirectory.dir("compose_compiler") }
text// WRONG // "Startup is 1420 ms after my change." // WRONG because: no variant, no device, no compilation mode, no iteration count — unreviewable. The same code measures 460 ms in release with R8 + a baseline profile on a Pixel 6.
text// RIGHT // "Startup TimeToInitialDisplay p50 = 460 ms (release, R8 on, baseline profile required, // Pixel 6 stock, cold start, 10 iterations, Macrobenchmark)."
CompilationMode.None, or from emulator-only runs are diagnostic at best.build/compose_compiler/<module>_release-*). Debug reports are corrupted by Live Literals.@TraceRecomposition counts (deterministic, runtime-instrumented). Use the latter for any conclusion.ndk { debugSymbolLevel = "FULL" } on the release variant so simpleperf, the Android Studio Profiler, and crash retrace work without rebuilding.release (or a release-derived build type), not debug.isMinifyEnabled = true and proguard-android-optimize.txt are present on that variant.release (Live Literals is off by default there in the Kotlin 2.0+ Compose Compiler DSL — there is no LiveLiterals feature flag to toggle)._release suffix in their names (app_release-composables.txt, etc.).CompilationMode.Partial(BaselineProfileMode.Require) and the target app installed for the run is the release variant.@TraceRecomposition (or Macrobenchmark FrameTimingMetric) on a release build.@TraceRecomposition): https://github.com/skydoves/compose-stability-analyzerOther measured skills in the registry, with their headline benchmark lift.