lazycolumn recomposes every visible row on scroll, layout inspector shows 400+ recomposition
900 item list, each row is an image, two text lines and a favourite button. Scrolling is visibly janky on a mid-range device and Layout Inspector shows recomposition counts in the 400s for rows after about ten seconds of flinging.
My understanding was that LazyColumn only composes what is visible and reuses slots as you scroll, so a row that scrolls off and comes back should be cheap. Instead everything appears to recompose constantly, including rows whose data has not changed at all.
Relevant bits:
LazyColumn {
items(state.rows) { row ->
RowCard(
row = row,
onTap = { viewModel.select(row.id) }
)
}
}
data class Row(val id: String, val title: String, val tags: List<String>)
The state comes from a StateFlow collected with collectAsStateWithLifecycle. What am I looking at?
@kraut_corner · 19h ago · 3 replies
You have three separate problems and they are all visible in that snippet.
1. No key. Without
key, an item's identity is its index. Any insertion, removal or reorder shifts everything and Compose throws away the state for every item after the change point. Add it:While you are there add
contentTypeif you have more than one row layout - it lets Compose reuse the right kind of slot instead of composing from scratch.2. A new lambda per item per composition.
onTap = { viewModel.select(row.id) }allocates a fresh lambda that capturesrow. New instance, different parameter,RowCardcannot skip. Hoist a single stable callback that takes the id:and have
RowCardcallonSelect(row.id)internally.3.
List<String>is an unstable type. The compiler cannot prove aListwill not be mutated under it, so any composable taking one is not skippable. Either mark the class@Immutable, or use a genuinely immutable collection type. Marking@Immutableis a promise you are making - if you break it, you get stale UI instead of slow UI, which is worse.Fix all three. Fixing one and remeasuring will disappoint you, because they compound.
To verify rather than hope, turn on composition tracing in the profiler, or drop a recomposition-counting modifier on the row in a debug build. Counts should drop to single digits for a full scroll.
Reply
Report
@two_calendars · 23h ago
All three, and the counts went from 400+ to 3. The lambda one is the one I would never have found - it looks like the most idiomatic possible code.
Reply
Report
@durable_ines · yesterday
It is idiomatic, which is why it is such a good trap. The rule that helped me remember: any lambda you write inside the item block is created fresh on every composition of that block. If it captures something from the loop it cannot be remembered as-is, so pass the identifier as a parameter instead of capturing it.
Reply
Report