santimattius/performance-compose-skills/compose-modifier-system
Claude Code and agent skill by santimattius/performance-compose-skills
- community
Checking that the SKILL.md file is still in the repository.
Jetpack Compose modifier system: modifier order semantics, ModifierNodeElement, custom Modifier.Node, layout phase internals, and the three drawing tiers (drawBehind, drawWithContent, drawWithCache). Trigger: when creating custom modifiers, working with modifier order, custom layout measurement, or draw-phase effects (graphicsLayer, drawBehind).
Install
npx vskill@latest install santimattius/performance-compose-skills/compose-modifier-systemSKILL.md
Performance First
Every Compose frame runs three phases. Cost rises left-to-right; restart scope shrinks left-to-right.
| Phase | What Runs | Restart Cost | Trigger | |
|---|---|---|---|---|
| Composition | Composable functions, state reads | HIGH (whole scope) | State read in composable body | |
| Layout | Measure + place | MEDIUM (subtree) | State read in measure lambda | |
| Drawing | Canvas commands, graphicsLayer | LOW (single node) | State read in draw/graphicsLayer lambda |
Defer State Reads (decision tree)
Do you need the value during composition?
- YES → read as
T(Composition phase) — accept full restart cost - NO → can you defer to layout/draw?
- Layout-only? → pass
() -> TintoModifier.layout/offset { }lambda - Draw/transform? → pass
() -> TintoModifier.graphicsLayer { }/drawBehind { }lambda
Rule: prefer Modifier.offset { lambda } over Modifier.offset(state.value.dp). Lambda variant defers the read to the layout phase, skipping recomposition entirely.
Lambda Overload vs Value Overload for Modifiers
The lambda form of offset { } creates its own layout-phase scope. State reads inside that lambda are invisible to the Composition phase — the composable does not recompose on scroll; only the layout subtree re-runs.
Performance Toolchain
WARNING: NEVER profile in debug builds. Debug disables R8, inlining, and Strong Skipping — numbers are meaningless. Always profile a release build withprofileableenabled or a benchmark build type.
| Tool | Use For | When | |
|---|---|---|---|
| Baseline Profiles | AOT-compile critical paths (startup, scroll) | Ship in release; regenerate per release | |
| Compose Compiler Reports | Detect unstable params, restartable/skippable status | Every PR; fail CI on new unstable types | |
| Layout Inspector (recomposition counts) | See which composables recompose and why | When debugging excess recomposition | |
| Composition Tracing | Frame-level composition timing in Android Studio | When Layout Inspector is not enough | |
| Macrobenchmark | Measure startup, frame timing, jank in release | Per-release regression gate |
When to Use
Use this skill when:
- Creating a custom modifier that draws (backgrounds, badges, shimmer, overlays)
- Creating a custom modifier that changes layout (custom padding, placement, sizing)
- Debugging why a modifier is re-running on every recomposition
- Deciding which drawing tier to use (
drawBehind,drawWithContent,drawWithCache) - Working with
Modifier.graphicsLayer { }for transforms or alpha - Implementing
ModifierNodeElement+Modifier.Nodeto replace legacyModifier.composed - Encountering a runtime crash related to measuring a child twice
CMP Applicability
Canonical CMP rules: `../_shared/cmp-platform.md`
| Source set | Status | Notes | |
|---|---|---|---|
commonMain | ✅ | All modifier APIs are commonMain-safe | |
androidMain | ✅ | Full skill content applies | |
iosMain | ✅ | graphicsLayer, drawBehind, Modifier.Node, ModifierNodeElement all supported | |
desktopMain | ✅ | Same as above | |
wasmJsMain | ✅ | Same as above |
Status legend: ✅ fully supported · ⚠️ partial / version-gated · ❌ Android-only.
If using in CMP: graphicsLayer, drawBehind, Modifier.layout, and ModifierNodeElement are all commonMain-safe. Custom draw modifiers using Canvas (Compose) APIs work on all CMP targets. Verify graphicsLayer lambda overload availability in your CMP version (CMP 1.6+). See `../_shared/cmp-platform.md`.
Critical Patterns
1. Modifier.composed Is DEPRECATED — Use ModifierNodeElement + Modifier.Node
Modifier.composed was the old way to create stateful or composable-aware modifiers. It is DEPRECATED because it allocates a new modifier on every recomposition and cannot be skipped by the Compose compiler.
For simple cases that only need CompositionLocal access or animation state, a @Composable factory function is the preferred lightweight alternative to Modifier.composed.
2. ModifierNodeElement MUST Be a data class
equals() / hashCode() correctness on ModifierNodeElement determines when update() is called vs. when a node is recreated. Without data class semantics, the node calls update() on every recomposition regardless of whether inputs changed.
3. shouldAutoInvalidate = false for Fine-Grained Invalidation
By default, Modifier.Node auto-invalidates (redraws + re-measures) when update() is called. Override shouldAutoInvalidate = false and call targeted invalidation methods to avoid unnecessary work.
Use invalidateDraw() for draw-only changes, invalidateMeasurement() for size changes, and invalidatePlacement() for position-only changes.
4. Three Drawing Tiers — Decision Tree
| Need | Use | Why | |
|---|---|---|---|
| Static background / simple shape | drawBehind | Lowest overhead; always draws behind content | |
| Full control of draw order relative to content | drawWithContent | Can draw before AND after drawContent() | |
Expensive objects reused across frames (Brush, Shader, Path) | drawWithCache | Cache invalidates on size or read-state change only |
5. graphicsLayer { } Lambda — ALWAYS Use the Lambda Overload
The property overload reads state during Composition. The lambda overload reads state during Drawing only.
6. CompositingStrategy Choices
graphicsLayer { alpha < 1f } creates an offscreen GPU buffer by default (Auto). Avoid when content does not overlap.
| Strategy | Buffer | Use When | |
|---|---|---|---|
Auto (default) | Yes, when alpha < 1f | Content overlaps and must composite correctly | |
ModulateAlpha | No | Content does NOT overlap (simpler views, icons) | |
Offscreen | Yes, always | BlendMode isolation is required (e.g., BlendMode.DstIn for masks) |
7. Single-Pass Measurement Rule
Compose enforces that each child is measured EXACTLY ONCE per layout pass. Measuring a child twice throws a runtime exception in debug builds.
Use intrinsics only when strictly necessary — they add a separate measurement pass.
8. Coordinates Are Pixels, Not Dp
All draw and layout operations inside DrawScope, MeasureScope, and placement blocks use pixel units. Always convert.
9. Modifier Order Is Semantically Significant
Modifiers are decorators. Each modifier wraps everything below it in the chain. The chain reads left-to-right; outer modifiers wrap inner ones.
Convention: always place the external modifier parameter FIRST on the UI root element, so callers can wrap the entire component.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/compose-modifier-system/SKILL.md
Popularity
- 7-day trend score
- 5
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/santimattius/performance-compose-skills/compose-modifier-system)