install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/new-silvermoon/awesome-android-agent-skills/compose-performance-audit" ~/.claude/skills/comeonoliver-skillshub-compose-performance-audit && rm -rf "$T"
manifest:
skills/new-silvermoon/awesome-android-agent-skills/compose-performance-audit/SKILL.mdsource content
Compose Performance Audit
Overview
Audit Jetpack Compose view performance end-to-end, from instrumentation and baselining to root-cause analysis and concrete remediation steps.
Workflow Decision Tree
- If the user provides code, start with "Code-First Review."
- If the user only describes symptoms, ask for minimal code/context, then do "Code-First Review."
- If code review is inconclusive, go to "Guide the User to Profile" and ask for Layout Inspector output or Perfetto traces.
1. Code-First Review
Collect:
- Target Composable code.
- Data flow: state, remember, derived state, ViewModel connections.
- Symptoms and reproduction steps.
Focus on:
- Recomposition storms from unstable parameters or broad state changes.
- Unstable keys in
/LazyColumn
(LazyRow
churn, missing keys).key - Heavy work in composition (formatting, sorting, filtering, object allocation).
- Unnecessary recompositions (missing
, unstable classes, lambdas).remember - Large images without proper sizing or async loading.
- Layout thrash (deep nesting, intrinsic measurements,
misuse).SubcomposeLayout
Provide:
- Likely root causes with code references.
- Suggested fixes and refactors.
- If needed, a minimal repro or instrumentation suggestion.
2. Guide the User to Profile
Explain how to collect data:
- Use Layout Inspector in Android Studio to see recomposition counts.
- Enable Recomposition Highlights in Compose tooling.
- Use Perfetto or System Trace for frame timing analysis.
- Check Macrobenchmark results for startup/scroll metrics.
Ask for:
- Layout Inspector screenshot showing recomposition counts.
- Perfetto trace or System Trace export.
- Device/OS/build configuration (debug vs release).
Important: Ensure profiling is done on a release build with R8 enabled. Debug builds have significant overhead.
3. Analyze and Diagnose
Prioritize likely Compose culprits:
- Recomposition storms from unstable parameters or broad state changes.
- Unstable keys in lazy lists (
churn, index-based keys).key - Heavy work in composition (formatting, sorting, object allocation).
- Missing
causing recreations on every recomposition.remember - Large images without
constraints.Modifier.size() - Unnecessary state reads in wrong composition phases.
Summarize findings with evidence from traces/Layout Inspector.
4. Remediate
Apply targeted fixes:
- Stabilize parameters: Use
or@Stable
annotations on data classes.@Immutable - Stabilize keys: Use stable, unique IDs for
/LazyColumn
items.LazyRow - Defer state reads: Use
, lambda-based modifiers, orderivedStateOf
.Modifier.drawBehind - Remember expensive computations: Wrap in
orremember { }
.remember(key) { } - Skip recomposition: Extract stable composables, use
to control identity.key() - Async image loading: Use Coil/Glide with proper sizing constraints.
- Reduce layout complexity: Flatten hierarchies, avoid deep nesting.
Common Code Smells (and Fixes)
Unstable lambda captures
// 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) { ... }
Expensive work in composition
// 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) { ... } } }
Missing keys in LazyColumn
// 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) } }
Unstable data classes
// 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 )
Reading state too early
// 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 }) { ... } }
Object allocation in composition
// 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)
Stability Checklist
| Type | Stable by Default? | Fix |
|---|---|---|
Primitives (, , ) | Yes | N/A |
with stable fields | Yes* | Ensure all fields are stable |
, , | No | Use from kotlinx |
Classes with properties | No | Use if externally stable |
| Lambdas | No | Use |
5. Verify
Ask the user to:
- Re-run Layout Inspector and compare recomposition counts.
- Run Macrobenchmark and compare frame timing.
- Test on a real device with release build.
Summarize the delta (recomposition count, frame drops, jank) if provided.
Outputs
Provide:
- A short metrics table (before/after if available).
- Top issues (ordered by impact).
- Proposed fixes with estimated effort.