$ Jetpack Compose Performance: 7 Pitfalls I Hit in Production
Compose makes it easy to write code that *looks* idiomatic and *runs* terribly. Here are the seven I now check for in every PR.
Compose is great. Compose is also a recomposition machine that will happily recompose your entire screen on every keystroke if you let it.
1. Reading state too high
Reading viewModel.uiState.collectAsState() in your top-level Scaffold recomposes everything. Push state reads down to the leaf that needs them.
2. Lambdas that capture state
// bad
Button(onClick = { count++ }) { ... }
// good — count is read inside the lambda, not captured
Button(onClick = onIncrement) { ... }
3. Lists without keys
items(list) re-keys on every change. items(list, key = { it.id }) enables structural identity.
4. Modifier chains as parameters
A fresh Modifier.padding(8.dp) is a new object every recomposition. Hoist it out or use remember.
5. derivedStateOf is not free
Use it when you have an expensive derivation read multiple times. Otherwise it adds overhead.
6. Animations driving recomposition
Modifier.offset { IntOffset(...) } (deferred read lambda) recomposes only the offset, not the whole composable. Modifier.offset(x = animatedValue) recomposes the whole tree.
7. Image loading on the main thread
Use Coil or Glide. Don't roll your own bitmap decoding inside a composable.
How I check
./gradlew assembleRelease -P composeCompilerReports=destination=... then read the module.txt report. If a function is restartable but not skippable, you have a problem.