Why Does Compose Recompose So Often? From Stability to State Read Placement
Frequent recomposition usually does not mean “Compose is slow.” More often, state is being read too high in the tree, parameters are not stable, or objects are being recreated in ways that prevent the runtime from skipping work.
After observed state changes, the affected scopes may need to run again. A function marked skippable can still recompose when its state is invalidated or its inputs differ. Check both state reads and the active compiler configuration.
State read location controls recomposition scope
The most common issue is reading frequently changing state in a parent and then passing derived values through multiple child components. List scroll state, text field input, countdown timers, and playback progress are typical examples. If they are read at the screen root, the whole screen becomes a recomposition candidate.
A better approach is to move the state read down to the place that actually needs it. If only one button needs loading, the whole screen should not read loading. If only the list header needs scroll offset, every item should not be aware of scroll state.
This is one of the most important ideas in Compose performance work: the goal is not to eliminate recomposition, but to shrink the area affected by it.
Why stability matters
Version note, September 30, 2026: Strong Skipping is enabled by default from Kotlin 2.0.20. Restartable Composables can be skippable even with unstable parameters: stable parameters use equality, while unstable ones use instance identity. Explicit opt-outs and runtime state invalidation still matter.
Plain mutable classes, standard collection interfaces, and external models can be inferred unstable. A new object is not automatically an unstable type, and lambdas are considered stable types. With Strong Skipping, lambdas inside Composables are automatically memoized by their captures. Do not infer a recomposition bug from syntax or an unstable label alone.
Read the updated report and state-read guide or organize a release report in the local report reader.
Do not overuse @Stable and @Immutable. They are not performance magic. They are contracts you make with Compose: either changes to the type are trackable, or the type is immutable. If the object can quietly change from the outside, adding the annotation only makes UI bugs harder to diagnose.
Lists amplify recomposition problems
In a LazyColumn, if items do not have stable keys, Compose has a harder time reusing existing item state when data is inserted, removed, or reordered. The symptoms can include more recomposition, lost scroll state, images reloading, or text field state jumping between rows.
List items should also avoid creating complex temporary objects inside the items block. Mapping a new list, creating a formatter, or assembling a large object on every recomposition can make item parameters look like they are constantly changing.
Recommended practices:
- Provide stable keys to
items. - Prepare UI models in the ViewModel layer; let Composables focus on rendering.
- Cache expensive objects with
remember, using accurate keys. - Pass frequently changing state only to the items that actually need it.
derivedStateOf is not a universal fix
derivedStateOf is useful when it compresses high-frequency input changes into low-frequency output changes. For example, the first visible item index can change repeatedly as different items enter view, while “should the back-to-top button be visible” only changes when it crosses the false/true boundary. That is a good use case.
Wrapping simple string concatenation or a basic field read in derivedStateOf only adds complexity. It is even worse when derivedStateOf is created in the wrong place and rebuilt on every recomposition, which removes the caching benefit and makes the code harder to read.
The decision rule is straightforward: consider derivedStateOf only when the inputs change many times and the output changes only a few times.
How to confirm recomposition is the problem
Do not rely only on how the UI feels. Android Studio’s Compose Layout Inspector can show recomposition counts, and Perfetto or system traces can show whether the main thread is being dominated by Compose runtime work, measure/layout, or draw. For suspected hotspots, you can temporarily add logs or use Compose compiler metrics to inspect stability inference.
If a screen janks but recomposition counts are low, the issue may be layout measurement, image decoding, main-thread I/O, or RenderThread work. If recomposition counts are high but each recomposition is cheap, recomposition still may not be the bottleneck. Recomposition is a signal, not a conclusion.
Engineering guardrails
Compose projects benefit from a few baseline rules: keep UI state as immutable as practical; require keys for lists; avoid reading high-frequency state at the screen root; move complex calculations into the ViewModel or remember; do not make shared components accept huge mutable objects; and keep recomposition observation tools available for performance-sensitive screens.
Frequent recomposition is rarely a single isolated bug. It is usually a state design and component boundary problem. Putting state in the right place is more effective than sprinkling remember across every Composable.