agentleFS
javiercamarenatriguerojaviercamarenatriguero/android-skills/compose-performance-auditor/SKILL.md

compose-performance-auditor

Skill8 starsChanged 4 months agoApache-2.0View on GitHub

Audits and fixes Jetpack Compose runtime performance issues. Use when user says "Compose is slow", "too many recompositions", "janky scrolling", "frame drops", or asks to "optimize Compose UI". Performs code-first review and guides profiling when needed.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When code review is inconclusive, collect runtime data:

    line 176
  2. Distinctive:

    Run app in debug mode

    line 180
  3. Very specific:

    Look for: jank frames (>16ms), long Choreographer frames, slow measure/layout/draw

    line 193

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Compose Performance Auditor

Overview

Audit Compose UI performance end-to-end: from code review to root-cause analysis and concrete fixes.

Workflow

Code available? ──Yes──▶ Code-First Review
      │
      No
      ▼
Describe symptoms ──▶ Ask for code/context ──▶ Code-First Review
      │
      If inconclusive
      ▼
Guide Profiling (Layout Inspector / Perfetto)

1. Code-First Review

What to Collect

  • Target composable code
  • Data flow: state, remember, ViewModel connections
  • Symptoms and reproduction steps

What to Look For

IssueSymptom
Recomposition stormsWhole tree recomposes on small state change
Unstable lambda capturesNew lambda instance every recomposition
Missing rememberExpensive object recreated every frame
Unstable class parametersCompose can't skip recomposition
Heavy work in compositionSorting / filtering / formatting during draw
Unstable keys in lazy listsItems re-render on unrelated changes
State read too earlyEntire composition recomposes instead of layout/draw phase
Large images without size constraintsDecoding full resolution unnecessarily
Deep nesting / intrinsic measurementsLayout pass is O(n²)

2. Common Code Smells and Fixes

Unstable Lambda Captures

// ❌ New lambda created every recomposition
Button(onClick = { viewModel.doAction(item.id) }) { Text("Action") }

// ✅ Stable reference
val onAction = remember(item.id) { { viewModel.doAction(item.id) } }
Button(onClick = onAction) { Text("Action") }

Expensive Work in Composition

// ❌ Sorts on every recomposition
@Composable fun ItemList(items: List<Item>) {
    val sorted = items.sortedBy { it.name }
    LazyColumn { items(sorted) { ItemRow(it) } }
}

// ✅ Computed once per unique input
@Composable fun ItemList(items: List<Item>) {
    val sorted = remember(items) { items.sortedBy { it.name } }
    LazyColumn { items(sorted) { ItemRow(it) } }
}

Missing Key in LazyColumn

// ❌ Index-based — redraws all items on insert/delete
LazyColumn { items(items) { ItemRow(it) } }

// ✅ Stable key — only changed items recompose
LazyColumn { items(items, key = { it.id }) { ItemRow(it) } }

Unstable Data Classes

// ❌ List<T> is unstable — Compose cannot skip recomposition
data class UiState(val items: List<Item>, val isLoading: Boolean)

// ✅ Annotate as @Immutable (if truly never mutated externally)
@Immutable
data class UiState(val items: List<Item>, val isLoading: Boolean)

// ✅ Or use kotlinx.collections.immutable
@Immutable
data class UiState(val items: ImmutableList<Item>, val isLoading: Boolean)

State Read Too Early (Defer to Layout/Draw Phase)

// ❌ Read during composition — whole tree recomposes on every scroll
@Composable fun ParallaxHeader(scrollState: ScrollState) {
    val offset = scrollState.value  // read in composition phase
    Image(modifier = Modifier.offset(y = offset.dp)) { ... }
}

// ✅ Defer read to layout phase
@Composable fun ParallaxHeader(scrollState: ScrollState) {
    Image(modifier = Modifier.offset { IntOffset(0, scrollState.value) }) { ... }
}

// ✅ Defer read to draw phase
@Composable fun ParallaxHeader(scrollState: ScrollState) {
    Box(modifier = Modifier.drawBehind {
        val y = scrollState.value.toFloat()
        drawRect(color = Color.Red, topLeft = Offset(0f, y))
    })
}

derivedStateOf Misuse

derivedStateOf only observes State<T> reads inside its lambda. Captured plain values (unwrapped via by) are not tracked.

// ❌ animatedValue is already a plain Float (unwrapped by `by`)
val animated by animateFloatAsState(target)
val text by remember { derivedStateOf { animated.toInt().toString() } }
// `text` never updates — animated is a Float, not a State<Float>

// ✅ Just compute directly; animateFloatAsState triggers recomposition anyway
val animated by animateFloatAsState(target)
val text = animated.toInt().toString()

// ✅ Or read the raw State object inside derivedStateOf
val animatedState = animateFloatAsState(target)  // no `by`
val text by remember { derivedStateOf { animatedState.value.toInt().toString() } }

Object Allocation in Composition

// ❌ New Modifier created every recomposition
Box(modifier = Modifier.padding(16.dp).clip(RoundedCornerShape(8.dp)))

// ✅ Remember static modifiers
val cardModifier = remember { Modifier.padding(16.dp).clip(RoundedCornerShape(8.dp)) }
Box(modifier = cardModifier)

3. Stability Checklist

TypeStable?Fix
Primitives (Int, String, Boolean, Float)✅ Yes—
data class with only stable fields✅ YesEnsure all fields stable
List, Map, Set❌ NoUse ImmutableList / @Immutable
Classes with var❌ NoAdd @Stable if contractually stable
Lambda {}❌ Noremember { } when capturing mutable state
Enum, sealed class✅ Yes—

4. Guide to Profiling

When code review is inconclusive, collect runtime data:

Layout Inspector (Android Studio)

  1. Run app in debug mode
  2. Open Layout Inspector → connect to process
  3. Enable Show Recomposition Counts
  4. Interact with the slow screen
  5. Identify composables with high recomposition counts

⚠️ Debug builds have overhead. Use for counting recompositions, not absolute timing.

Perfetto / System Trace

  1. Build a release build (R8 enabled, no debug overhead)
  2. Open Android Studio → Profiler → CPU → Record
  3. Choose System Trace or export to Perfetto
  4. Look for: jank frames (>16ms), long Choreographer frames, slow measure/layout/draw

Macrobenchmark

For repeatable, CI-comparable metrics:

@Test
fun scrollBenchmark() = benchmarkRule.measureRepeated(
    packageName = "com.example.myapp",
    metrics = listOf(FrameTimingMetric()),
    startupMode = StartupMode.COLD,
) {
    pressHome()
    startActivityAndWait()
    device.findObject(By.res("list")).scroll(Direction.DOWN, 10)
}

5. Output Format

Report findings as:

## Compose Performance Report

### Summary
- X recomposition storms detected
- Y unstable types found
- Z missing `remember` blocks

### Findings (ordered by impact)

#### 1. [High] Unstable lambda in `ItemList`
- File: src/.../ItemList.kt:42
- Issue: New lambda created every recomposition
- Fix: Wrap in `remember(item.id) { { ... } }`

#### 2. [Medium] Missing key in LazyColumn
...

### Estimated Impact
- Before: ~30 recompositions/interaction
- After (estimated): ~5 recompositions/interaction

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.