---
title: "Compose Compiler Metrics: Fix Recomposition Bugs Fast"
published: true
description: "Learn to parse Compose compiler reports, identify stability inference failures, fix unstable classes, and reclaim your 16ms frame budget."
tags: android, kotlin, mobile, architecture
canonical_url: https://mvpfactory.co/blog/compose-compiler-metrics-recomposition
---
## What You Will Build
By the end of this tutorial you will have compiler-generated `.txt` metric files wired into your build, the vocabulary to read them, and a repeatable process for eliminating unnecessary recompositions before you ever open a profiler. In a mid-size e-commerce app with 87 composables and a heavy list-based UI, fixing three unstable data classes cut recompositions per scroll gesture from 340 down to 91 — a 73% reduction with zero business logic changes.
## Prerequisites
- Kotlin Gradle Plugin 1.8+
- Compose Compiler 1.5+
- AGP 8+
---
## Step 1: Enable the Compiler Report
Add this to your `build.gradle.kts`. This is the minimal setup to get this working:
kotlin
tasks.withType().configureEach {
compilerOptions {
freeCompilerArgs.addAll(
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.layout.buildDirectory.get().asFile.absolutePath + "/compose_reports",
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.layout.buildDirectory.get().asFile.absolutePath + "/compose_metrics"
)
}
}
After a build, check `build/compose_reports/` for four files: `*-classes.txt`, `*-composables.txt`, `*-composables.csv`, and `*-module.json`.
---
## Step 2: Read the Composables Report
Open `*-composables.txt`. The compiler annotates every composable with its stability verdict:
kotlin
restartable skippable scheme("[androidx.compose.ui.UiComposable]")
fun ProductCard(
stable product: Product
stable onClick: Function0
)
restartable scheme("[androidx.compose.ui.UiComposable]")
fun CartRow(
unstable cartItem: CartItem
stable onRemove: Function0
)
`CartRow` is restartable but **not skippable**. One unstable parameter blocks the entire composable. Every time the parent recomposes, `CartRow` follows — regardless of whether `cartItem` actually changed.
Here is the pattern I use in every project to reason about this:
| Status | Can Skip Unchanged Inputs | Recomposes on Parent Recompose |
|---|---|---|
| Restartable + Skippable | Yes | No |
| Restartable only | No | Yes |
| Not restartable | No | Inline in parent |
Non-restartable is a **compiler decision**, not a developer target. Focus your work on moving composables from restartable-only into restartable-and-skippable.
---
## Step 3: Hunt Stability Inference Failures in `*-classes.txt`
kotlin
unstable class CartItem {
stable val id: String
stable val quantity: Int
unstable val price: BigDecimal
= Unstable
}
`BigDecimal` is not on the Compose stability allowlist, so the entire class fails. The most common offenders in production are `BigDecimal`, `Date`, `Calendar`, and `java.util` collections (`List`, `Map`, `Set` — replace with `kotlinx.collections.immutable`).
**If you own the class**, annotate it directly:
kotlin
@Immutable
data class CartItem(
val id: String,
val quantity: Int,
val price: BigDecimal
)
**If you don't own it**, add it to a stability config file:
properties
// stability_config.conf
com.squareup.moshi.JsonClass
java.math.BigDecimal
Reference it in Gradle: `stabilityConfigurationFile = rootProject.file("stability_config.conf")`.
---
## Step 4: Trace the Cascade
The docs do not mention this explicitly, but one unstable class at the top of your tree can invalidate an entire subtree on every state write. Take a `CheckoutScreen` receiving an unstable `OrderSummary`. It hosts `PricingBreakdown`, `DeliveryOptions`, and `ConfirmButton` — none of which changed. Because `CheckoutScreen` is non-skippable, all three children recompose on every parent invalidation. If `OrderSummary` is rebuilt from a `StateFlow` on every emission, you are triggering that cascade on every keystroke in a nearby search field sharing the same composition scope. In a production app with 120 composables, I measured a single non-skippable screen-level composable generating over 300 unnecessary recompositions per second during a list scroll — 11ms of a 16ms frame budget, gone.
The compiler report tells you *which parameter* is blocking skippability. The runtime profiler only shows you the symptom. Fix it at the source.
---
## Gotchas
- **AGP 8+ deprecates `project.buildDir`** — use `layout.buildDirectory` as shown above, or the path will resolve incorrectly.
- **`@Stable` is a promise, not a guarantee.** If you annotate a class `@Stable` but mutate it outside of Compose state, you will silently skip recompositions that should have happened.
- **`kotlinx.collections.immutable` is not a drop-in `List` replacement.** You need to update call sites to `toPersistentList()` — easy to miss in ViewModels that return plain `listOf()`.
- **Non-restartable composables are not your target.** Do not try to "achieve" non-restartable status. The compiler decides that for cheap, side-effect-free composables. Annotating toward it manually is a waste of effort.
---
## Three Things to Do Now
1. **Wire compiler reports into CI and diff `*-composables.txt` on PRs.** A new non-skippable composable in a feature branch should be a blocking comment, not a post-ship discovery.
2. **Audit every class that crosses a composable boundary.** Any `unstable` class in a screen-level parameter list is a guaranteed regression at scale.
3. **Set a recomposition budget per screen.** Target fewer than 5 recompositions per second on idle screens in Perfetto. If more than 20% of your composables are non-skippable, you have structural stability problems — no amount of `remember {}` will save you.
I keep a recomposition budget checklist in my notes — right next to the HealthyDesk reminder that fires every 45 minutes telling me to stand up. Both exist because the cost of ignoring a small chronic drain is surprisingly high.
**Further reading:** [Compose compiler metrics — official docs](https://developer.android.com/develop/ui/compose/performance/stability/diagnose) | [kotlinx.collections.immutable](https://github.com/Kotlin/kotlinx.collections.immutable)
Top comments (0)