· 7 min · Android, Compose, Performance
Jetpack Compose Performance: What Actually Moves the Needle
Recomposition counts are a vanity metric. Here is what I actually measure before shipping a Compose screen to millions of users — and the three fixes that made the biggest difference in production.
When AppMeddy crossed 10M+ users, every dropped frame became a support ticket. Compose makes UI fast to write, but "fast to write" and "fast to run" are different budgets. This post covers the profiling workflow I use before any release.
Measure first, always
Layout Inspector recomposition counts are a starting point, not a verdict. A composable recomposing 40 times is fine if each pass is cheap. What matters is frame time on a mid-range device — I test on a Redmi with 4GB RAM, because that is what a large share of real users hold.
The three fixes that mattered
First: stability. Most "mystery" recompositions trace back to unstable lambdas and List<T> parameters. Wrapping collections in ImmutableList and hoisting lambdas fixed more jank than any other change.
// Unstable: a new lambda + new List instance every recomposition
@Composable
fun ProductListScreen(products: List<Product>, onClick: (Product) -> Unit) { ... }
// Stable: skips recomposition when nothing actually changed
@Composable
fun ProductListScreen(
products: ImmutableList<Product>,
onClick: (Product) -> Unit
) {
LazyColumn {
items(products, key = { it.id }) { product ->
ProductRow(product, onClick = remember(product.id) { { onClick(product) } })
}
}
}
Second: `derivedStateOf` for scroll-driven UI. A toolbar reacting to scroll position without it recomposes on every pixel. With it, only on threshold crossings — a 60x reduction in one screen.
Third: defer reads. Passing a State<T> down and reading it in the deepest child keeps recomposition scope tiny. Passing the value recomposes the whole subtree.
Profile before you refactor — the bottleneck is almost never where intuition points.
What did not matter
Micro-optimizing modifier order, replacing Column with ConstraintLayout, and remember-ing trivial calculations produced no measurable difference.