0
0
0
0
0
0
0
0
0
0
0
0

· 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.

← All entries · Portfolio