$npx -y skills add skydoves/compose-performance-skills --skill deferring-state-readsUse 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 {
| 1 | # Deferring State Reads — Move Hot Reads from Composition to Draw |
| 2 | |
| 3 | Compose runs three phases per frame — **Composition → Layout → Draw**. A state read at phase N invalidates phase N and every phase below it. The single biggest perf win in any animation- or scroll-driven UI is moving a state read from Composition down to Layout or Draw via a lambda-based modifier. This skill teaches Claude how to spot the wrong-phase read and migrate it. |
| 4 | |
| 5 | ## When to use this skill |
| 6 | |
| 7 | - An animation triggers full subtree recomposition on every frame (`Modifier.alpha(progress.value)`, `Modifier.offset(x.dp)`, `Modifier.padding(state.dp)` — `padding` has no lambda overload, so use `Modifier.layout { ... }` or `Modifier.offset { IntOffset(...) }` instead when the inset is animated). |
| 8 | - A scroll position is read directly in a composable body (`val y = scrollState.value`) and the parent recomposes on every pixel of scroll. |
| 9 | - The developer says "every frame", "scroll jank", "animation jank", "dropped frames", or reports the whole screen recomposing on drag. |
| 10 | - A `@TraceRecomposition` log shows the parent's recomposition counter incrementing once per animation tick. |
| 11 | - The developer is passing a hot value (animation progress, scroll offset, drag delta) as a `Float` parameter across composables. |
| 12 | |
| 13 | ## When NOT to use this skill |
| 14 | |
| 15 | - The state changes once per user interaction (button click, dialog open) — the lambda-modifier rewrite buys nothing and adds noise. |
| 16 | - The state read genuinely needs to drive Composition (show/hide a different composable, swap a different component tree). Lambda modifiers cannot decide which composable to emit. |
| 17 | - The non-skippable parent is non-skippable for a different reason (unstable parameter). Diagnose with `../../stability/diagnosing-compose-stability/SKILL.md` first. |
| 18 | - The developer wants to filter many high-frequency inputs into one rare boolean — that is `../choosing-derivedstateof/SKILL.md`. |
| 19 | |
| 20 | ## Prerequisites |
| 21 | |
| 22 | - Familiarity with the three-phase model. If unsure which phase a read happens in, read `references/three-phases.md` before doing the migration. |
| 23 | - Compose UI **1.7+** for `rememberGraphicsLayer()` and the modern `Modifier.animateItem()` shape. Lambda forms of `Modifier.offset { }`, `Modifier.graphicsLayer { }`, `Modifier.drawBehind { }`, and `Modifier.drawWithCache { }` exist on every supported Compose version. |
| 24 | - Ability to confirm recomposition counts in Layout Inspector or via `@TraceRecomposition` (see `../debugging-recompositions/SKILL.md` if it exists in this repo, otherwise the skydoves `compose-stability-analyzer` runtime). |
| 25 | - A **release** build for any final measurement — debug builds run interpreted and lie about cost. |
| 26 | |
| 27 | ## Workflow |
| 28 | |
| 29 | - [ ] **1. Identify the state read site and the modifier consuming it.** Look for `state.value`, `by state`, `animatedFloat.value`, `scrollState.value`, `dragOffset.value` in a composable body. Note which modifier consumes it. |
| 30 | |
| 31 | - [ ] **2. Classify the modifier — value form or lambda form.** Most positioning/sizing/drawing modifiers ship in two flavors: |
| 32 | |
| 33 | | Value form (read in Composition) | Lambda form (read in Layout or Draw) | |
| 34 | |---|---| |
| 35 | | `Modifier.offset(x = dp, y = dp)` | `Modifier.offset { IntOffset(x, y) }` | |
| 36 | | `Modifier.padding(start = dp, …)` | `Modifier.layout { measurable, constraints -> … }` (no lambda overload of `padding`; use a custom layout to inset with hot state) | |
| 37 | | `Modifier.size(dp)` | `Modifier.layout { … }` (custom) | |
| 38 | | `Modifier.alpha(float)` | `Modifier.graphicsLayer { alpha = … }` | |
| 39 | | `Modifier.rotate(float)` | `Modifier.graphicsLayer { rotationZ = … }` | |
| 40 | | `Modifier.scale(float)` | `Modifier.graphicsLayer { scaleX = …; scaleY = … }` | |
| 41 | | `Modifier.background(color)` | `Modifier.drawBehind { drawRect(color) }` | |
| 42 | |
| 43 | If a value form is being fed a hot state, swap to the lambda form. The lambda is invoked on every Layout (or Draw) pass without re-running Compo |