๐ŸŽจ Jetpack Compose

Compose Performance Optimization

Compose is fast when you work with its model and slow when you fight it. This guide shows how to measure, find unnecessary recomposition, fix stability issues, defer state reads to cheaper phases, and tune lazy lists, in order of impact.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Advanced
On this page
  1. Step 0: Measure correctly
  2. Understand the three phases
  3. The fixes, in order of impact
  4. Tools checklist
  5. Typical fixes in the wild
  6. Common mistakes
  7. Frequently asked questions
  8. Conclusion

"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 FrameTimingMetric for scrolling and animation jank.
  • Use Layout Inspector's recomposition counts to see which composables recompose and how often.

Understand the three phases

Reading state in later phases avoids recomposition State read during composition triggers composition, layout and drawing. State read in a layout lambda, such as Modifier.offset with a lambda, triggers only layout and drawing. State read in a draw lambda, such as drawBehind or graphicsLayer, triggers only drawing, the cheapest. Compositionmost expensive Layoutmeasure + place Drawingcheapest Text("$offset")reads โ†’ all 3 phases Modifier.offset { โ€ฆ }reads โ†’ layout + draw graphicsLayer { alpha = โ€ฆ }reads โ†’ draw only Read fast-changing state as late as possible.
Where you read state determines which phases re-run. For animations and scroll effects, prefer lambda-based modifiers.

The fixes, in order of impact

1. Defer reads of fast-changing state

Collapsing headerKotlin
// โŒ 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

derivedStateOfKotlin
val showFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }

3. Don't do expensive work in composition

remember with keys, or move to the ViewModelKotlin
// โŒ 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 key and contentType for every items call.
  • 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:

Stable UI modelsKotlin
// 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

Extract the frequently-changing partKotlin
@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

QuestionTool
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 derivedStateOf for 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.

See all our apps โ†’

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.

Share: LinkedIn X / Twitter WhatsApp