On this page
"Compose is slow" usually means one of a few specific problems: testing a debug build, recomposing a large tree on every scroll frame, or doing expensive work inside composables. All of them are measurable and fixable.
Step 0: Measure correctly
- Use a release-like build (minified, non-debuggable). Debug Compose is significantly slower.
- Add a Baseline Profile. Compose ships its own profile rules, and your app's critical journeys benefit from one too. See Baseline Profiles.
- Use Macrobenchmark with
FrameTimingMetricfor scrolling and animation jank. - Use Layout Inspector's recomposition counts to see which composables recompose and how often.
Understand the three phases
The fixes, in order of impact
1. Defer reads of fast-changing state
// โ Recomposes Header on every scroll pixel
Header(Modifier.offset(y = (-scrollState.value / 2).dp))
// โ
Reads scroll only in the layout phase
Header(Modifier.offset { IntOffset(0, -scrollState.value / 2) })
// โ
Alpha in the draw phase
Header(Modifier.graphicsLayer { alpha = 1f - (scrollState.value / 600f).coerceIn(0f, 1f) })2. Use derivedStateOf when outputs change less than inputs
val showFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }3. Don't do expensive work in composition
// โ Sorting and formatting 1,000 items on every recomposition
val rows = transactions.sortedByDescending { it.date }.map { it.toUi() }
// โ
Recompute only when the input changes (better still: do it in the ViewModel)
val rows = remember(transactions) { transactions.sortedByDescending { it.date }.map { it.toUi() } }4. Tune lazy lists
- Provide
keyandcontentTypefor everyitemscall. - Keep item composables small and parameters simple (IDs, strings, immutable UI models).
- Size images to their display size, and avoid decoding full-resolution bitmaps.
5. Stability and strong skipping
Compose skips a composable when its parameters are equal to last time and considered stable. With strong skipping mode, the default in recent Compose compiler versions, composables with unstable parameters can still be skipped, using instance equality for those parameters, and lambdas are remembered automatically. This removed many earlier stability headaches, but a few things still matter:
// Prefer immutable data classes for UI state
@Immutable
data class ProductUi(val id: String, val title: String, val priceText: String)
// A List parameter is an interface Compose can't prove immutable.
// With strong skipping, a NEW list instance on each emission means "changed".
// Avoid recreating identical lists; consider kotlinx.collections.immutable for clarity.Use the Compose compiler's stability reports to see which classes are unstable. Fix the ones that appear on hot paths, not all of them.
6. Narrow recomposition scope
@Composable
fun PlayerScreen(state: PlayerState) {
Column {
AlbumArt(state.art) // rarely changes
TrackInfo(state.title, state.artist) // rarely changes
ProgressBar(progressProvider = { state.progress }) // lambda: read deferred into ProgressBar
}
}Passing a () -> Float provider instead of the value lets only ProgressBar react to every progress tick.
Tools checklist
| Question | Tool |
|---|---|
| Is scrolling janky? | Macrobenchmark FrameTimingMetric, Perfetto trace |
| What recomposes too often? | Layout Inspector recomposition counts and highlighting |
| Which classes are unstable? | Compose compiler stability reports |
| Is startup slow? | Macrobenchmark StartupTimingMetric, Baseline Profile comparison |
Typical fixes in the wild
Feed jank
Missing keys, large images and formatting in item bodies. Fixed with keys, sized images and preformatted UI models.
Collapsing toolbar stutter
Scroll offset read in composition. Fixed with lambda offset and graphicsLayer.
Whole screen recomposes per keystroke
Text state hoisted too high and read at the root. Fixed by narrowing reads to the field and the components that need them.
Common mistakes
- Optimising stability everywhere before measuring.
- Backwards writes: writing state that was already read in the same composition, which triggers recomposition loops.
- Using
derivedStateOffor cheap derivations that change as often as their inputs. - Testing on a debug build and concluding Compose is slow.
Frequently asked questions
Is recomposition itself bad?
No. It's how Compose works. Excessive recomposition of large subtrees on every frame is the problem.
Do I need @Stable and @Immutable everywhere?
No. With strong skipping and immutable data classes, most code needs no annotations. Use them deliberately where reports show a hot-path problem.
How much do Baseline Profiles help Compose?
They often noticeably improve startup and first-scroll smoothness because Compose code is large. Measure the gain with Macrobenchmark in your own app.
Conclusion
Fast Compose comes from measuring release builds, reading fast-changing state late, keeping composition cheap, tuning lazy lists and fixing stability where it matters. Baseline Profiles and Macrobenchmark prove the improvement.
Next step: build components Compose doesn't ship with in Custom Layouts and Drawing in Compose.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Performance work starts with understanding how Android apps are built and how they behave at runtime. Strengthen those fundamentals and the profiling tools start to make sense.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.