Audit and improve Jetpack Compose runtime performance from code review and architecture. Use when asked to diagnose slow rendering, janky scrolling, excessive recompositions, or performance issues in Compose UI.
96
95%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Audit Jetpack Compose view performance end-to-end, from instrumentation and baselining to root-cause analysis and concrete remediation steps.
Collect:
Focus on:
LazyColumn/LazyRow (key churn, missing keys).remember, unstable classes, lambdas).SubcomposeLayout misuse).Provide:
Explain how to collect data:
Ask for:
Important: Ensure profiling is done on a release build with R8 enabled. Debug builds have significant overhead.
Prioritize likely Compose culprits:
key churn, index-based keys).remember causing recreations on every recomposition.Modifier.size() constraints.Summarize findings with evidence from traces/Layout Inspector.
Apply targeted fixes:
@Stable or @Immutable annotations on data classes.LazyColumn/LazyRow items.derivedStateOf, lambda-based modifiers, or Modifier.drawBehind.remember { } or remember(key) { }.key() to control identity.// BAD: New lambda instance every recomposition
Button(onClick = { viewModel.doSomething(item) }) { ... }
// GOOD: Use remember or method reference
val onClick = remember(item) { { viewModel.doSomething(item) } }
Button(onClick = onClick) { ... }// BAD: Sorting on every recomposition
@Composable
fun ItemList(items: List<Item>) {
val sorted = items.sortedBy { it.name } // Runs every recomposition
LazyColumn { items(sorted) { ... } }
}
// GOOD: Use remember with key
@Composable
fun ItemList(items: List<Item>) {
val sorted = remember(items) { items.sortedBy { it.name } }
LazyColumn { items(sorted) { ... } }
}// BAD: Index-based identity (causes recomposition on list changes)
LazyColumn {
items(items) { item -> ItemRow(item) }
}
// GOOD: Stable key-based identity
LazyColumn {
items(items, key = { it.id }) { item -> ItemRow(item) }
}// BAD: Unstable (contains List, which is not stable)
data class UiState(
val items: List<Item>,
val isLoading: Boolean
)
// GOOD: Mark as Immutable if truly immutable
@Immutable
data class UiState(
val items: ImmutableList<Item>, // kotlinx.collections.immutable
val isLoading: Boolean
)// BAD: State read during composition (recomposes whole tree)
@Composable
fun AnimatedBox(scrollState: ScrollState) {
val offset = scrollState.value // Recomposes on every scroll
Box(modifier = Modifier.offset(y = offset.dp)) { ... }
}
// GOOD: Defer state read to layout/draw phase
@Composable
fun AnimatedBox(scrollState: ScrollState) {
Box(modifier = Modifier.offset {
IntOffset(0, scrollState.value) // Read in layout phase
}) { ... }
}// BAD: Creates new Modifier chain every recomposition
Box(modifier = Modifier.padding(16.dp).background(Color.Red))
// GOOD for dynamic modifiers: Remember the modifier
val modifier = remember { Modifier.padding(16.dp).background(Color.Red) }
Box(modifier = modifier)| Type | Stable by Default? | Fix |
|---|---|---|
Primitives (Int, String, Boolean) | Yes | N/A |
data class with stable fields | Yes* | Ensure all fields are stable |
List, Map, Set | No | Use ImmutableList from kotlinx |
Classes with var properties | No | Use @Stable if externally stable |
| Lambdas | No | Use remember { } |
Ask the user to:
Summarize the delta (recomposition count, frame drops, jank) if provided.
Provide:
3f68e39
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.