Practical guidance for diagnosing and fixing Compose recomposition performance issues using Rebound's compiler plugin, runtime, IDE plugin, and CLI tooling.
Workflow
When helping with Compose recomposition performance, follow this checklist:
1. Understand the problem
- Is this a setup question, a violation diagnosis, or a stability optimization?
- Is the user looking at CLI output, IDE data, or logcat warnings?
- What platform? Android, iOS simulator, iOS physical device, or KMP?
2. Consult the right reference
Read the relevant reference file(s) from references/ before answering:
| Topic | Reference File |
|---|---|
Budget classes, thresholds, color coding, dynamic scaling, @ReboundBudget |
references/budget-classes.md |
| Violation patterns per budget class, diagnostic workflow, code fixes | references/diagnosing-violations.md |
| CLI commands, JSON snapshot structure, connection modes, netcat queries | references/cli-usage.md |
| IDE 5-tab cockpit, editor integration, connection setup | references/ide-plugin.md |
| Gradle plugin setup, config options, KMP, iOS relay, Kotlin version matrix | references/setup-guide.md |
Skip rate formula, $changed bitmask, param states, forced vs param-driven, fix strategies |
references/skip-rate-stability.md |
3. Diagnose before fixing
Follow this diagnostic sequence:
- Get the data — Run
./rebound-cli.sh snapshotor check the IDE Hot Spots tab - Identify the violator — Which composable exceeds its budget? What's its budget class?
- Check skip rate — Low skip rate (<50%) means parameters keep changing. High skip rate (>80%) with high rate means forced recomposition from parent.
- Check param states — Look at
paramStatesandparamTypesin the snapshot. Are paramsDIFFERENT? Are theyunstableorlambda? - Check the call tree — Is this composable being forced by a parent? Check the
parentfield andforcedCount. - Apply the fix — Consult
references/diagnosing-violations.mdfor the pattern that matches.
4. Apply and verify
- Write the minimal correct fix — don't over-engineer
- Re-run
./rebound-cli.sh snapshotor check the IDE to verify the fix reduced the rate - Flag any anti-patterns you see in the user's existing code
Key Concepts
-
Every
@Composablegets a budget. Rebound auto-classifies composables into 7 budget classes (SCREEN, LEAF, CONTAINER, INTERACTIVE, LIST_ITEM, ANIMATED, UNKNOWN) based on IR heuristics. Each has a max recompositions/second threshold. -
Budgets scale dynamically. During scrolling (2x), animation (1.5x), or user input (1.5x), budgets increase to avoid false positives.
-
Skip rate reveals the problem type. Low skip rate = unstable params causing unnecessary work. High skip rate + high enter rate = parent forcing recomposition on children that correctly skip.
-
The
$changedbitmask tells you exactly which params changed. 3 bits per parameter: UNCERTAIN (000), SAME (001), DIFFERENT (010), STATIC (100). This is the ground truth for why a composable recomposed. -
Forced vs param-driven matters. Forced means a parent invalidated and all children re-enter. Param-driven means this composable's own inputs changed. Different root causes, different fixes.
Common Quick Fixes
| Symptom | Likely Cause | Fix |
|---|---|---|
| SCREEN at >3/s | Reading frequently-changing state at screen level | Hoist state down to the child that needs it |
| LEAF at >5/s | Unstable parameter (data class without @Stable) |
Add @Stable/@Immutable or wrap in remember |
| CONTAINER at >10/s | Unstable lambda passed as child content | Use remember { } around the lambda |
| Low skip rate (<20%) | Most params are unstable or lambda type |
Stabilize data classes, remember lambdas |
| High forced count | Parent recomposing unnecessarily | Fix the parent first — violations cascade down |
| LIST_ITEM at >60/s during scroll | Item composable doing heavy work | Move heavy computation to remember or ViewModel |