avoiding-subcomposition-pitfalls
Avoiding Subcomposition Pitfalls — Keep Composition Out of the Measure Pass
SubcomposeLayout runs its content's composition during the measure pass, not during the parent's composition pass. That trades extra layout cost for the ability to use measured constraints inside the children's composition. BoxWithConstraints, material3.Scaffold, and the lazy layouts are all built on top of it, so the cost is invisible until it nests or lands inside a hot path. This skill teaches Claude how to detect the misuse, how to replace SubcomposeLayout with a cheaper primitive when its power is not needed, and how to keep it efficient when it is.
When to use this skill
- The developer wraps content in
BoxWithConstraintspurely to readmaxWidth/maxHeightand pick between a few composables, and the screen feels heavy at first frame or on configuration change (rotation, IME show). - A
BoxWithConstraintsorScaffoldis nested inside anotherBoxWithConstraintsorScaffold, multiplying subcomposition cost. - A
BoxWithConstraintssits inside aLazyColumn/LazyRowitem and the developer reports scroll jank that did not exist before the wrap was added. - The developer manually authors a
SubcomposeLayoutblock but only uses constraints to compute final positions, never to drive child composition. - The Layout Inspector or a Perfetto trace shows
Compose:applyChangesor measure-phase composition counts that scale with parent constraint changes (rotation, drag-to-resize, animated container size). - A custom
SubcomposeLayout'smeasurePolicyblock allocates a fresh composable lambda insidesubcompose(slotId) { … }on every measurement. AndroidX maintains an internal lint check namedComposableLambdaInMeasurePolicy(incompose/lint/internal-lint-checks/) that flags exactly this pattern on its own codebase; published APIs likeBoxWithConstraints,material.Scaffold, andmaterial3.TabRowsuppress that check because the trade-off is fundamental to their public contract. App-side code that reproduces the pattern pays the same cost without the suppression rationale.
When NOT to use this skill
- The developer needs to compose a child whose content depends on another child's measured size — that is the canonical
SubcomposeLayoutuse case and there is no cheaper primitive. KeepSubcomposeLayout; tune it (see Pattern: Tuning a justified SubcomposeLayout). - The slow path is inside a
LazyColumnitem but the cause is unstable parameters, not subcomposition. Diagnose with../../stability/diagnosing-compose-stability/SKILL.mdfirst. - The state read driving repeated measurement is an animation value rather than a structural constraint. That is
../deferring-state-reads/SKILL.md. - The user wants to optimize lazy list prefetch — that is
../../lists/configuring-lazy-prefetch/SKILL.md.
More from skydoves/compose-performance-skills
diagnosing-compose-stability
Use this skill to diagnose Jetpack Compose stability problems by enabling and reading the Compose Compiler Reports (classes.txt, composables.txt, composables.csv, module.json). Covers the Gradle DSL, the release-only build requirement, and how to interpret per-class and per-composable stability annotations including stable, unstable, runtime, restartable, skippable, readonly, @static, and @dynamic markers. Use when the developer asks "why does this recompose", reports jank, dropped frames, slow scroll, high recomposition count, suspects an unstable parameter, mentions Compose Compiler Reports, classes.txt, composables.txt, module.json, or wants to know which composables are non-skippable. The fix lives in a sibling skill — this one only diagnoses.
10deferring-state-reads
Use this skill to push frequently-changing Jetpack Compose state reads (scroll position, animation values, drag offsets) out of the Composition phase and down into Layout or Draw using lambda-based modifiers like Modifier.offset { }, Modifier.layout { }, Modifier.graphicsLayer { }, Modifier.drawBehind { }, and Modifier.drawWithCache { }. Covers the three-phase model (Composition, Layout, Draw), why a state read at phase N invalidates phase N and every phase below, the modifier-phase cheat sheet, and lambda providers (() -> T) for hoisting hot values across composables. Use when the developer mentions every-frame work, scroll jank, animation jank, dropped frames, animated alpha or offset, "the whole subtree recomposes on scroll", Modifier.alpha(state.value), Modifier.offset(x.dp), or graphicsLayer.
10collecting-flows-safely
Use this skill to migrate Compose UI from `collectAsState()` to `collectAsStateWithLifecycle()`, hoist `Flow<T>` parameters out of composables, and apply `.conflate()` / `.distinctUntilChanged()` / `snapshotFlow` so background CPU and battery stop draining and chatty flows stop invalidating the UI per emission. Covers ViewModel `StateFlow`/`SharedFlow` consumers, sensor and location streams, and the "Flow as composable parameter" antipattern. Trigger when the user mentions `collectAsState`, `collectAsStateWithLifecycle`, lifecycle-aware flow collection, `Lifecycle.State.STARTED`, background battery drain from a Compose screen, `snapshotFlow`, `Flow` parameter on a composable, conflate, or distinctUntilChanged.
10debugging-recompositions
Use this skill to find which Jetpack Compose composables are recomposing and why, using Android Studio Layout Inspector recomposition counts and skip counts, the per-parameter Argument Change Reasons (Changed / Unchanged / Uncertain / Static / Unknown) introduced in Android Studio Hedgehog and later, and runtime `@TraceRecomposition` from `compose-stability-analyzer` for production-like measurement. Walks through enabling counts, mapping each Argument Change Reason to a fix, and confirming the result in a release build. Use when the developer says "this should be skipping but isn't", "I want to see recomposition counts", asks what "Uncertain" or "Unknown" means in the inspector, or needs to confirm a stability or strong-skipping fix actually worked end-to-end.
10auditing-compose-performance
Use this skill to run an end-to-end Jetpack Compose performance audit when the symptom is broad ("the app feels sluggish", "scroll is rough everywhere", "we're starting a perf sprint", "what should we fix first?"). Orchestrates the four-phase Measure → Diagnose → Fix → Verify loop by sequencing the 25 focused skills (release-mode setup, R8, Baseline Profiles, Compose Compiler reports, stability inference, Layout Inspector, `@TraceRecomposition`, stabilization, strong skipping, phase-deferral, derivedStateOf, lazy layouts, lazy prefetch, Modifier.Node, modifier ordering, flow collection, effects, CI gates, hot-reload) and produces a written audit report with Before/After Macrobenchmark numbers. Use when the developer wants a perf sprint kickoff, a pre-release perf gate, onboarding to a perf-troubled codebase, or a written deliverable. Use when the user mentions "audit", "perf review", "perf sprint", "where do I start", or has no specific symptom yet.
10configuring-lazy-prefetch
Use this skill to tune Jetpack Compose lazy-layout prefetch with LazyLayoutCacheWindow (Compose Foundation 1.9+, @ExperimentalFoundationApi) and pausable composition in prefetch (Compose Foundation 1.10+, default on). Covers configurable Dp-based ahead/behind cache windows plumbed through rememberLazyListState(cacheWindow = ...), NestedPrefetchScope for items containing inner lazy layouts (HorizontalPager inside a LazyColumn row), version requirements, and the trade-off between memory pressure and idle-frame work. Use when the developer mentions dropped frames at high scroll velocity, prefetch window, ahead/behind extents, LazyLayoutCacheWindow, NestedPrefetchScope, pausable composition for prefetch, or wants composition retained for items briefly scrolled past. Item-level fixes (keys, contentType) live in a sibling skill.
9