santimattius/performance-compose-skills/compose-composition-core
Claude Code and agent skill by santimattius/performance-compose-skills
- community
Checking that the SKILL.md file is still in the repository.
Jetpack Compose composition internals: state management, recomposition mechanics, stability, component identity, CompositionLocal, and the 3-phase performance model. Trigger: when working with Compose state, recomposition bugs, derivedStateOf, remember variants, stability annotations, LazyList performance, or CompositionLocal.
Install
npx vskill@latest install santimattius/performance-compose-skills/compose-composition-coreSKILL.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.
State Hoisting + Lambda-Deferred Read
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:
- A composable recomposes more than expected (excess invalidation, jank)
- Deciding between
remember,rememberSaveable,retain, orrememberSerializable - Using
derivedStateOfand unsure if it is the right tool - Debugging unexpected state loss after configuration changes or process death
- Working with
CompositionLocaland unsure which variant to choose - Optimizing
LazyColumn/LazyRowfor reorder or insert/delete - Reviewing stability of custom classes and deciding when to apply
@Stable/@Immutable
When to Use This vs compose-effects
compose-composition-core covers state lifecycle, recomposition mechanics, and stability decisions — everything that happens during the Composition phase and its identity model.
compose-effects covers LaunchedEffect, DisposableEffect, SideEffect, and snapshotFlow — operations triggered BY composition but that run outside it (coroutines, subscriptions, cleanup).
Use both when: reading a StateFlow inside a composable (collectAsStateWithLifecycle bridges them), or when derivedStateOf inside remember is combined with snapshotFlow.
CMP Applicability
Canonical CMP rules: `../_shared/cmp-platform.md`
| Source set | Status | Notes | |
|---|---|---|---|
commonMain | ✅ | All patterns fully supported | |
androidMain | ✅ | Full skill content applies | |
iosMain | ✅ | remember, derivedStateOf, CompositionLocal all commonMain-safe | |
desktopMain | ✅ | Same as above | |
wasmJsMain | ✅ | Same as above |
Status legend: ✅ fully supported · ⚠️ partial / version-gated · ❌ Android-only.
If using in CMP: Watch for CMP-REMEMBER-PLATFORM-LEAK — never capture android.content.Context, Activity, or UIViewController inside remember { } in commonMain. These are platform types and will cause compilation failures on non-Android targets. Use expect/actual to provide platform context where needed.
Critical Patterns
1. 3-Phase Lambda Deferral
The fastest recomposition is the one that never happens. Pass () -> T when the parent holds state but the child is the only one that renders it.
Only use provider lambdas (() -> T) when you have a measured performance problem. They add lambda allocation overhead and reduce readability. Always measure before and after.
2. derivedStateOf — Only When Output Changes Less Than Input
derivedStateOf creates a snapshot state whose recomposition scope fires only when the RESULT changes, not every time an input changes.
Key rule: component parameters are NOT snapshot state — they cannot be observed by derivedStateOf. When derivedStateOf depends on a parameter, that parameter MUST be a remember key.
3. Backwards-Write Warning
CRITICAL: Writing MutableState inside a composable body triggers another recomposition immediately, creating an infinite loop. This is one of the most dangerous patterns in Compose.
4. remember(key) for Expensive Computations
remember without keys is a permanent cache — it never updates if inputs change.
Do NOT add snapshot state reads as keys. Snapshot state is observed automatically inside the remember lambda; adding it as a key triggers full reinitialization instead of the cheaper read-update cycle.
5. LazyList Key — Mandatory for Correct Identity
Without a key, LazyColumn uses index-based identity. Inserting or removing an item shifts all indices below it, causing full recomposition of every shifted item.
Key constraints: must be unique among siblings; on Android, must be a Bundle-storable type (String, Int, etc.); must remain stable for the same logical item.
6. Modifier.offset { lambda } vs Modifier.offset(value)
See Pattern 1 and the Code Examples section for the full contrast. The short rule: any rapidly-changing state (animations, scroll offsets) that affects only position/transform belongs in a lambda overload to confine the read to the Layout or Drawing phase.
7. State Lifespan Ladder
| Function | Survives Recomposition | Survives Config Change | Survives Process Death | Accepts Non-Serializable | |
|---|---|---|---|---|---|
remember | Yes | No | No | Yes | |
retain | Yes | Yes | No | Yes | |
rememberSaveable | Yes | Yes | Yes (Bundle types) | No (needs Saver) | |
rememberSerializable | Yes | Yes | Yes (Java serialization) | No |
- Use
rememberfor transient UI state (expansion, focus, interaction source). - Use
retainfor heavy non-serializable objects that must survive rotation (e.g.,ExoPlayer). - Use
rememberSaveablefor user-entered input and UI state that must survive system-initiated process death. - Use
rememberSerializableonly when you have no other option — Java serialization is fragile.
Stability note for `retain`: available in Compose runtime snapshots; verify your BOM version before use.rememberSerializableis not yet in stable Compose as of 2026-05; check BOM release notes.
8. Strong Skipping Mode
Default since Kotlin 2.0.20. Key behavior changes:
- Unstable types are now compared by reference (
===) instead of being treated as always-changed. This eliminates many false recompositions. - ALL lambdas in composable functions are automatically wrapped in
remember { }with their captured variables as keys. Lambda identity is stable unless captured variables change. - To opt out of lambda memoization for a specific lambda, annotate with
@DontMemoize.
Profiling note: debug builds disable Strong Skipping. ALWAYS profile release builds.
9. CompositionLocal
CompositionLocal propagates values implicitly down the composition tree. Choose the variant based on how often the value changes.
| Variant | Recomposition on change | Use When | |
|---|---|---|---|
compositionLocalOf | Only direct readers recompose | Value changes at runtime (theme colors, locale) | |
staticCompositionLocalOf | ENTIRE subtree recomposes | Value is effectively constant after first provision |
Do NOT useCompositionLocalas a substitute for parameter passing in non-cross-cutting concerns. Reserve it for framework-level values (theme, locale, window info) that would otherwise require threading through many composable layers.
Pitfalls
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 reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/compose-composition-core/SKILL.md
Popularity
- Repository stars
- 1
- Repository forks
- 1
- 7-day trend score
- 8
Stars and forks are for the whole GitHub repository, not this skill alone. 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-composition-core)