Use this skill when a Compose tree uses SubcomposeLayout, BoxWithConstraints, or Scaffold and the developer reports extra measure passes, slow first frame, or layout passes running content composition repeatedly. Covers why SubcomposeLayout composes its slots during the measure phase, why BoxWithConstraints forces a subcomposition for every new Constraints value, why nesting Scaffold or BoxWithConstraints multiplies the cost, when a custom Layout or Modifier.layout { } replaces SubcomposeLayout, and how to use SubcomposeLayoutState's slot reuse policy and precompose APIs when SubcomposeLayout is genuinely required. Use when the developer mentions BoxWithConstraints, SubcomposeLayout, Scaffold, "extra measure pass", "double measurement", "first frame slow", "subcompose", or notices that wrapping content in BoxWithConstraints regresses scroll perf inside a LazyColumn.
AI 与智能体513recomposition/avoiding-subcomposition-pitfalls/SKILL.md
Install
Send this to Claude Code, Codex or Cursor. The agent checks the Skill for safety first and installs it only after you confirm.
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 BoxWithConstraints purely to read maxWidth / maxHeight and pick between a few composables, and the screen feels heavy at first frame or on configuration change (rotation, IME show).
A BoxWithConstraints or Scaffold is nested inside another BoxWithConstraints or Scaffold, multiplying subcomposition cost.
A BoxWithConstraints sits inside a LazyColumn/LazyRow item and the developer reports scroll jank that did not exist before the wrap was added.
The developer manually authors a SubcomposeLayout block but only uses constraints to compute final positions, never to drive child composition.
The Layout Inspector or a Perfetto trace shows Compose:applyChanges or measure-phase composition counts that scale with parent constraint changes (rotation, drag-to-resize, animated container size).
A custom SubcomposeLayout's measurePolicy block allocates a fresh composable lambda inside subcompose(slotId) { … } on every measurement. AndroidX maintains an internal lint check named ComposableLambdaInMeasurePolicy (in compose/lint/internal-lint-checks/) that flags exactly this pattern on its own codebase; published APIs like BoxWithConstraints, material.Scaffold, and material3.TabRow suppress 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 SubcomposeLayout use case and there is no cheaper primitive. Keep SubcomposeLayout; tune it (see Pattern: Tuning a justified SubcomposeLayout).
The slow path is inside a LazyColumn item but the cause is unstable parameters, not subcomposition. Diagnose with ../../stability/diagnosing-compose-stability/SKILL.md first.
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.
Prerequisites
Familiarity with Compose's three phases (Composition, Layout, Draw). If unsure, read ../deferring-state-reads/references/three-phases.md first.
A release build with R8 for any final measurement. ../../measurement/testing-compose-in-release-mode/SKILL.md covers why debug builds lie.
Layout Inspector or Perfetto trace tooling. ../../measurement/tracing-recompositions-at-runtime/SKILL.md covers wiring androidx.compose.runtime:runtime-tracing so that measure-phase work shows up in system traces.
1. Identify every SubcomposeLayout user in the hot path. Grep the affected screen for BoxWithConstraints, Scaffold, SubcomposeLayout, and any custom @Composable that delegates to one. The material3.Scaffold and foundation.layout.BoxWithConstraints are both implemented with SubcomposeLayout; their cost is identical.
2. For each user, decide whether the constraints are actually consumed during composition. A BoxWithConstraints that reads maxWidth only to pass it to a child as a parameter is wasting the subcomposition machinery. A BoxWithConstraints that picks between two completely different @Composable trees based on maxWidth < 600.dp is using it correctly.
3. Replace unjustified BoxWithConstraints with a cheaper primitive. If the consumer only needs the final size in pixels (e.g. to compute an offset, an alpha, a draw command), use Modifier.layout { measurable, constraints -> … } or Modifier.onSizeChanged { … } instead. Both run during the layout phase without spinning up a new composition.
4. Avoid nesting subcomposing layouts. Two Scaffolds nested, or a BoxWithConstraints inside a Scaffold slot that itself contains another BoxWithConstraints, multiplies measure-phase composition. Hoist the outer one to the screen root and use ordinary Box / Column / Row for the inner branches.
5. Never wrap a LazyColumn/LazyRow item in BoxWithConstraints. Each item subcomposes during measurement. If the item needs the available width, pass it down once from the parent (the LazyColumn already knows its constraints) or use Modifier.fillMaxWidth().onSizeChanged { … } for one-time sizing.
6. When SubcomposeLayout is justified, configure SubcomposeLayoutState(SubcomposeSlotReusePolicy(n)). The state object retains up to n previously-used slots so the next subcomposition reuses their slot table and node subtree instead of paying for a fresh composition. The factory SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse: Int) ships in androidx.compose.ui.layout.
7. For predictable upcoming slots, call state.precompose(slotId, content) on a previous frame. It returns a PrecomposedSlotHandle; the next subcompose(slotId, …) during measurement skips the composition step. This is exactly how the lazy layouts spread item composition across frames.
8. Verify in a Perfetto system trace. Add androidx.compose.runtime:runtime-tracing and confirm that Compose:recompose and Compose:applyChanges no longer fire during scroll or resize ticks at the rate of the parent's constraint changes. The measured screen should show subcomposition only on actual structural change (e.g. rotation), not on every frame.
Patterns
Pattern: BoxWithConstraints used to read size — replace with Modifier.onSizeChanged
// WRONG
@Composable
fun ParallaxHeader(scrollOffset: () -> Float) {
BoxWithConstraints {
val widthPx = with(LocalDensity.current) { maxWidth.toPx() }
Image(
painter = painterResource(R.drawable.hero),
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.graphicsLayer {
translationX = scrollOffset() * widthPx * 0.2f
},
contentDescription = null,
)
}
}
// WRONG because: BoxWithConstraints subcomposes its content during the measure phase. Each
// new constraint (rotation, IME show, animated parent size) re-runs that subcomposition just
// to read maxWidth, when a Modifier.onSizeChanged would have given the same value with no
// extra composition.
onSizeChanged runs in the layout phase (no subcomposition); the underlying onRemeasured is invoked on every layout pass and forwards to the user callback when the measured size differs from the previous size. The graphicsLayer block runs in Draw with the latest widthPx and the latest scroll offset.
Pattern: BoxWithConstraints to pick a layout — keep it but hoist it
// WRONG
@Composable
fun ProductCard(product: Product) {
BoxWithConstraints {
if (maxWidth < 360.dp) {
CompactProductCard(product)
} else {
WideProductCard(product)
}
}
}
@Composable
fun ProductGrid(products: List<Product>) {
LazyVerticalGrid(columns = GridCells.Fixed(2)) {
items(products) { ProductCard(it) }
}
}
// WRONG because: every grid cell wraps its content in BoxWithConstraints. Each cell pays
// for a subcomposition during the grid's measure pass. With 200 cells visible across a long
// scroll session, that is 200 redundant subcompositions on every measurement.
// RIGHT
@Composable
fun ProductGrid(products: List<Product>) {
BoxWithConstraints {
val isCompact = maxWidth / 2 < 360.dp
LazyVerticalGrid(columns = GridCells.Fixed(2)) {
items(products) { product ->
if (isCompact) CompactProductCard(product) else WideProductCard(product)
}
}
}
}
The single BoxWithConstraints at the grid root subcomposes once per parent constraint change. The decision flows into every item as a plain Boolean, which is stable and skippable.
Pattern: Nested Scaffold — collapse to one
// WRONG
@Composable
fun ProfileScreen(...) {
Scaffold(topBar = { TopBar(...) }) { outer ->
Scaffold(
modifier = Modifier.padding(outer),
bottomBar = { ProfileTabBar(...) },
) { inner ->
ProfileContent(modifier = Modifier.padding(inner))
}
}
}
// WRONG because: each Scaffold is a SubcomposeLayout with its own measure-phase composition.
// Nesting them doubles that cost on every frame whose constraints change (IME show, rotation,
// nav bar inset change).
Scaffold already supports a top bar, bottom bar, snackbar host, and FAB in a single subcomposition. Use the slots provided rather than nesting another Scaffold.
Pattern: Tuning a justified SubcomposeLayout
When a screen genuinely needs measure-time access to constraints (a tab strip whose tabs size by their own content while sharing the row's max width, a lookahead-driven shared element container, a custom popover that aligns to a measured anchor), keep SubcomposeLayout but tune the state.
@Composable
fun BadgeRow(badges: List<Badge>) {
val slotState = remember {
SubcomposeLayoutState(SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse = 8))
}
SubcomposeLayout(state = slotState) { constraints ->
val measurables = subcompose(slotId = "badges") {
badges.fastForEach { Badge(it) }
}
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.sumOf { it.width }
val height = placeables.maxOf { it.height }
layout(width, height) {
var x = 0
placeables.fastForEach { p ->
p.place(x, 0)
x += p.width
}
}
}
}
SubcomposeSlotReusePolicy(8) lets the layout retain up to eight previously-composed slots. When a slot id reappears (e.g. a badge enters the screen again after exiting), the layout reuses the existing slot table instead of rerunning composition. The lazy layouts use a similar mechanism with their own policy implementation.
For predictable upcoming slots, call state.precompose(slotId, content) on an earlier frame and dispose the returned PrecomposedSlotHandle if the slot is not needed after all. The next subcompose(slotId, …) during measurement reuses the precomposed content with no composition step.
// WRONG
LazyColumn {
items(rows) { row ->
BoxWithConstraints {
val isWide = maxWidth > 600.dp
RowContent(row, isWide)
}
}
}
// WRONG because: each row item subcomposes during the LazyColumn's measure pass. Scroll the
// list and every newly-visible item pays for a fresh subcomposition. On a 60Hz feed this is
// the difference between smooth and dropped frames.
The single BoxWithConstraints at the feed root subcomposes only when the feed itself is remeasured. The per-row item is a plain RowContent call with a stable Boolean parameter, which strong skipping handles.
Mandatory rules
MUST NOT use BoxWithConstraints only to read maxWidth/maxHeight for a Modifier-level effect. Use Modifier.onSizeChanged { … } or Modifier.layout { … } instead.
MUST NOT place a BoxWithConstraints inside a LazyColumn/LazyRow/LazyVerticalGrid item. Hoist the constraint read to the lazy layout's parent and pass the resolved value down.
MUST NOT nest Scaffold inside another Scaffold. Use the slots (topBar, bottomBar, snackbarHost, floatingActionButton) the outer Scaffold already exposes.
MUST keep a SubcomposeLayout when the children's composition genuinely depends on a measured value (parent constraints, sibling size). The point is to use the right tool, not to ban SubcomposeLayout.
MUST pass a SubcomposeSlotReusePolicy to SubcomposeLayoutState whenever slots come and go. The default no-op policy disposes slots eagerly and pays full composition cost on every reappearance.
PREFERRED: for predictable upcoming slots, call state.precompose(slotId, content) on a previous frame and let the measure-pass subcompose reuse the result.
PREFERRED: measure once with Modifier.onSizeChanged { … } over reading maxWidth in a BoxWithConstraints body when the value only feeds a graphicsLayer { }, drawBehind { }, or other Layout/Draw-phase consumer.
Verification
A Perfetto system trace recorded with androidx.compose.runtime:runtime-tracing shows that Compose:recompose and Compose:applyChanges no longer fire on every measurement of the affected subtree. They should fire only on real structural change (rotation, navigation, list item insert).
Layout Inspector's recomposition count for any subtree wrapped by the prior BoxWithConstraints does not increment per parent constraint change.
In any retained custom SubcomposeLayout, the subcompose(slotId) { … } content lambda is hoisted into a remember-stable reference (or is cheap and intentional, with a one-line comment explaining why a fresh lambda per measurement is safe here). This is the pattern AndroidX's internal ComposableLambdaInMeasurePolicy lint enforces on the AndroidX codebase.
A grep over the migrated file finds zero BoxWithConstraints blocks inside LazyColumn/LazyRow/LazyVerticalGrid items.
No Scaffold is nested inside another Scaffold in the migrated file.
For every retained SubcomposeLayout, a SubcomposeSlotReusePolicy is passed to SubcomposeLayoutState, OR the comment explicitly notes that all slots are permanent and reuse is unnecessary.
../deferring-state-reads/references/three-phases.md — three-phase model background that motivates "do not run composition during measure".
../../lists/configuring-lazy-prefetch/SKILL.md — the lazy layouts use precompose and pausable subcomposition; the same APIs are available to custom SubcomposeLayout users.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Stop and check this skill before finishing any reply to a question about how to use Claude or a Claude product — it recommends matching courses, tutorials, and use cases from Claude Academy (academy.claude.com), Anthropic's learning hub. Trigger on: "how do I", "how can I", "getting started with", "what can Claude do", "teach me", "learn to use"; questions about artifacts, projects, skills, plugins, connectors, MCP; requests about rolling Claude out to a team, class, or organization; and any ask for training materials, onboarding content, or learning resources. Use it when the user is learning how to use a feature or product — not when they are mid-task and just want the task done. This skill composes with other skills: after consulting product documentation to answer how a Claude feature works, also check here for a matching course or tutorial — a docs-grounded answer and an Academy recommendation belong together. Only recommend on a strong match; never invent Academy content.