$npx -y skills add hanamizuki/solopreneur --skill compose-performance-auditAudit 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.
| 1 | # Compose Performance Audit |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | Audit Jetpack Compose view performance end-to-end, from instrumentation and baselining to root-cause analysis and concrete remediation steps. |
| 6 | |
| 7 | ## Workflow Decision Tree |
| 8 | |
| 9 | - If the user provides code, start with "Code-First Review." |
| 10 | - If the user only describes symptoms, ask for minimal code/context, then do "Code-First Review." |
| 11 | - If code review is inconclusive, go to "Guide the User to Profile" and ask for Layout Inspector output or Perfetto traces. |
| 12 | |
| 13 | ## 1. Code-First Review |
| 14 | |
| 15 | Collect: |
| 16 | - Target Composable code. |
| 17 | - Data flow: state, remember, derived state, ViewModel connections. |
| 18 | - Symptoms and reproduction steps. |
| 19 | |
| 20 | Focus on: |
| 21 | - **Recomposition storms** from unstable parameters or broad state changes. |
| 22 | - **Unstable keys** in `LazyColumn`/`LazyRow` (`key` churn, missing keys). |
| 23 | - **Heavy work in composition** (formatting, sorting, filtering, object allocation). |
| 24 | - **Unnecessary recompositions** (missing `remember`, unstable classes, lambdas). |
| 25 | - **Large images** without proper sizing or async loading. |
| 26 | - **Layout thrash** (deep nesting, intrinsic measurements, `SubcomposeLayout` misuse). |
| 27 | |
| 28 | Provide: |
| 29 | - Likely root causes with code references. |
| 30 | - Suggested fixes and refactors. |
| 31 | - If needed, a minimal repro or instrumentation suggestion. |
| 32 | |
| 33 | ## 2. Guide the User to Profile |
| 34 | |
| 35 | Explain how to collect data: |
| 36 | - Use **Layout Inspector** in Android Studio to see recomposition counts. |
| 37 | - Enable **Recomposition Highlights** in Compose tooling. |
| 38 | - Use **Perfetto** or **System Trace** for frame timing analysis. |
| 39 | - Check **Macrobenchmark** results for startup/scroll metrics. |
| 40 | |
| 41 | Ask for: |
| 42 | - Layout Inspector screenshot showing recomposition counts. |
| 43 | - Perfetto trace or System Trace export. |
| 44 | - Device/OS/build configuration (debug vs release). |
| 45 | |
| 46 | > **Important**: Ensure profiling is done on a **release build** with R8 enabled. Debug builds have significant overhead. |
| 47 | |
| 48 | ## 3. Analyze and Diagnose |
| 49 | |
| 50 | Prioritize likely Compose culprits: |
| 51 | - **Recomposition storms** from unstable parameters or broad state changes. |
| 52 | - **Unstable keys** in lazy lists (`key` churn, index-based keys). |
| 53 | - **Heavy work in composition** (formatting, sorting, object allocation). |
| 54 | - **Missing `remember`** causing recreations on every recomposition. |
| 55 | - **Large images** without `Modifier.size()` constraints. |
| 56 | - **Unnecessary state reads** in wrong composition phases. |
| 57 | |
| 58 | Summarize findings with evidence from traces/Layout Inspector. |
| 59 | |
| 60 | ## 4. Remediate |
| 61 | |
| 62 | Apply targeted fixes: |
| 63 | - **Stabilize parameters**: Use `@Stable` or `@Immutable` annotations on data classes. |
| 64 | - **Stabilize keys**: Use stable, unique IDs for `LazyColumn`/`LazyRow` items. |
| 65 | - **Defer state reads**: Use `derivedStateOf`, lambda-based modifiers, or `Modifier.drawBehind`. |
| 66 | - **Remember expensive computations**: Wrap in `remember { }` or `remember(key) { }`. |
| 67 | - **Skip recomposition**: Extract stable composables, use `key()` to control identity. |
| 68 | - **Async image loading**: Use Coil/Glide with proper sizing constraints. |
| 69 | - **Reduce layout complexity**: Flatten hierarchies, avoid deep nesting. |
| 70 | |
| 71 | ## Common Code Smells (and Fixes) |
| 72 | |
| 73 | ### Unstable lambda captures |
| 74 | |
| 75 | ```kotlin |
| 76 | // BAD: New lambda instance every recomposition |
| 77 | Button(onClick = { viewModel.doSomething(item) }) { ... } |
| 78 | |
| 79 | // GOOD: Use remember or method reference |
| 80 | val onClick = remember(item) { { viewModel.doSomething(item) } } |
| 81 | Button(onClick = onClick) { ... } |
| 82 | ``` |
| 83 | |
| 84 | ### Expensive work in composition |
| 85 | |
| 86 | ```kotlin |
| 87 | // BAD: Sorting on every recomposition |
| 88 | @Composable |
| 89 | fun ItemList(items: List<Item>) { |
| 90 | val sorted = items.sortedBy { it.name } // Runs every recomposition |
| 91 | LazyColumn { items(sorted) { ... } } |
| 92 | } |
| 93 | |
| 94 | // GOOD: Use remember with key |
| 95 | @Composable |
| 96 | fun ItemList(items: List<Item>) { |
| 97 | val sorted = remember(items) { items.sortedBy { it.name } } |
| 98 | LazyColumn { items(sorted) { ... } } |
| 99 | } |
| 100 | ``` |
| 101 | |
| 102 | ### Missing keys in LazyColumn |
| 103 | |
| 104 | ```kotlin |
| 105 | // BAD: Index-based identity (causes recomposition on list changes) |
| 106 | LazyColumn { |
| 107 | items(items) { item -> ItemRow(item) } |
| 108 | } |
| 109 | |
| 110 | // GOOD: Stable key-based identity |
| 111 | LazyColumn { |
| 112 | items(items, key = { it.id }) { item -> ItemRow(item) } |
| 113 | } |
| 114 | ``` |
| 115 | |
| 116 | ### Unstable data classes |
| 117 | |
| 118 | ```kotlin |
| 119 | // BAD: Unstable (contains List, which is not stable) |
| 120 | data class UiState( |
| 121 | val items: List<Item>, |
| 122 | val isLoading: Boolean |
| 123 | ) |
| 124 | |
| 125 | // GOOD: Mark as Immutable if truly immutable |
| 126 | @Immutable |
| 127 | data class UiState( |
| 128 | val items: ImmutableList<Item>, // kotlinx.collections.immutable |
| 129 | val isLoading: Boolean |
| 130 | ) |
| 131 | ``` |
| 132 | |
| 133 | ### Reading state too early |
| 134 | |
| 135 | ```kotlin |
| 136 | // BAD: State read during composition (recomposes whole tree) |
| 137 | @Composable |
| 138 | fun AnimatedBox(scrollState: ScrollState) { |
| 139 | val offset = scrollState.value // Recomposes on every scroll |
| 140 | Box(modifier = Modifier.offset(y = offset.dp)) { ... } |
| 141 | } |
| 142 | |
| 143 | // GOOD: |