Compose performance
Skill almasumdev/awesome-kotlin-android-agent-skills/.github/skills/performance/compose-performance
Curated agent skills, conventions, and workflows for building Kotlin Android apps with AI coding agents.
npx -y skills add almasumdev/awesome-kotlin-android-agent-skills --skill compose-performanceAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Expert guidance on diagnosing and fixing Jetpack Compose performance problems using recomposition tracing, stability/immutability annotations, and Compose compiler metrics. Use this for jank, slow lists, or recomposition complaints.
SKILL.md
5.8 KB, as published. Nobody here has run it
Compose Performance — Stability, Recomposition, Metrics
Instructions
Compose is fast when it can skip. Most performance complaints are stability problems in disguise. Measure first; fix the root cause.
1. Generate Compose Compiler Reports
Add to each Compose module's build.gradle.kts:
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions.freeCompilerArgs.addAll(
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${layout.buildDirectory.get()}/compose-reports",
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${layout.buildDirectory.get()}/compose-metrics",
)
}
Build and inspect build/compose-reports/*-classes.txt. You want:
- Every data class used as Compose state:
stable. - Every screen composable:
skippableandrestartable. - Zero
unstable parameter:lines in hot paths.
2. Common Instability Causes and Fixes
| Cause | Fix |
|---|---|
List<T> parameter | Use ImmutableList<T> from kotlinx.collections.immutable. |
LocalDateTime, LocalDate | Annotate a wrapper @Immutable data class or map to Long. |
| Types from third-party libs | Wrap in your own @Immutable class. |
var field in a data class used as state | Change to val; create a new instance to update. |
| Function-type parameter (lambda) capturing unstable value | Hoist lambda into remember(key) { { ... } } or accept a stable action/state. |
3. Recomposition Counts in Layout Inspector
Android Studio → Layout Inspector → enable "Show recomposition counts" on a running app. Scroll your screen; any composable with growing counts during a static state is re-running too often. Pair with compiler reports to pinpoint the unstable parameter.
4. Common Fixes Cookbook
a) Stable lambda
// BAD — new lambda identity on every recomposition, may defeat skipping downstream
ArticleList(items, onClick = { id -> vm.onAction(Open(id)) })
// GOOD — stable reference
val onClick: (String) -> Unit = remember(vm) { { id -> vm.onAction(Open(id)) } }
ArticleList(items, onClick = onClick)
Do this only where the report shows restartable skippable skipped: NO. Premature lambda hoisting bloats code for no gain.
b) Immutable lists
data class ArticlesUiState(val items: ImmutableList<Article> = persistentListOf())
c) derivedStateOf to avoid reading too much state
val canSubmit by remember { derivedStateOf { form.name.isNotBlank() && form.agree } }
Without derivedStateOf, every form change recomposes the submit button; with it, only when the boolean actually changes.
d) Keyed LazyColumn items
items(items = list, key = { it.id }, contentType = { "article" }) { Article(it) }
Missing key causes "reset" recomposition on list changes — every visible item rebuilds.
e) Defer reads with lambdas
// BAD — text recomposes whole parent
Text("Count: ${state.count}")
// GOOD — only Text recomposes
Text(text = { "Count: ${state.count}" }.toString()) // when accepting the value
// Or, for graphicsLayer, pass a lambda that reads state inside the block:
Modifier.graphicsLayer { alpha = state.alpha }
Prefer Modifier.graphicsLayer { ... } (block form) over Modifier.alpha(state.alpha) for animated values — the state read moves to draw phase, skipping composition and layout.
5. Baseline Profiles
Generate a Baseline Profile to speed up critical-path composables AOT:
// baseline-profile module
@OptIn(ExperimentalBaselineProfilesApi::class)
class BaselineProfileGenerator {
@get:Rule val rule = BaselineProfileRule()
@Test
fun startup() = rule.collect(packageName = "com.example.app") {
pressHome(); startActivityAndWait()
device.findObject(By.res("feed_list")).fling(Direction.DOWN)
}
}
Run with the Baseline Profile Gradle Plugin. Ship the generated baseline-prof.txt with the app.
6. Rules of Thumb
- 60/120 Hz budget: each frame = 16 ms / 8 ms. Keep composition + layout + draw under that for visible screens.
- Prefer
@Composableextraction only when it creates a stable boundary. Tiny helpers that take unstable inputs aren't helping. - Do not call
remember { expensive() }without a key — if inputs change the value is wrong. Useremember(inputs) { }. SnapshotStateList(frommutableStateListOf) is great for local UI state but is unstable as a function parameter unless declared explicitly; preferImmutableListfor cross-boundary.
7. Macrobenchmark
@get:Rule val benchmarkRule = MacrobenchmarkRule()
@Test
fun scrollFeed() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(FrameTimingMetric()),
iterations = 10,
startupMode = StartupMode.WARM,
compilationMode = CompilationMode.Partial(BaselineProfileMode.Require),
) {
startActivityAndWait()
device.findObject(By.res("feed_list")).fling(Direction.DOWN)
}
Run in CI for every release and treat frameDurationCpuMs P95 > 16 ms as a regression.
Checklist
- Compose compiler reports generated for every Compose module.
- No screen-level composable has
restartable skippable skipped: NOin the report. - Lists use
ImmutableList/persistentListOf(). -
LazyColumn/LazyRowitems all passkey. - Animated values use
Modifier.graphicsLayer { }block form. - A Baseline Profile is generated and bundled with the release build.
- Macrobenchmark frame-timing tests run in CI.