Compose performance is one question asked in four places
Compose performance work reduces to knowing which of composition, layout and draw is being invalidated, and why. The diagnostic order matters more than any individual optimisation.

Day 78 of 100, opening the performance pillar. Day 10 introduced the three phases; this post is about using them as a diagnostic rather than a fact.
The symptom
A list that janks, and a fix applied by guesswork:
LazyColumn {
items(orders, key = { it.id }) { order ->
OrderRow(order)
}
}
Scrolling drops frames. The team tries the usual moves in the usual order: add
derivedStateOf somewhere, wrap things in remember, mark a data class @Immutable,
split the row into smaller composables. Some help, some don't, and nobody can say why.
Two weeks later the list is faster and the codebase has a dozen optimisations nobody can justify removing.
Why the obvious approach fails
The obvious approach is a checklist of Compose performance tips applied top to bottom.
It fails because those tips address different phases. derivedStateOf reduces
composition invalidations. @Immutable enables skipping when a composable is re-invoked.
A lambda modifier moves a read into layout. Baseline Profiles affect JIT compilation, not
recomposition at all.
Applying a composition fix to a draw-phase problem does nothing, and you learn nothing from it not working.

The actual mechanism
Every Compose performance problem is one of four things, and they're worth separating because the fixes don't overlap:
1. Composition running when it shouldn't. A scope is invalidated by a state read, or a child can't skip because a parameter is unstable. Days 79 and 80.
2. Composition being expensive when it does run. Work in a composable body that isn't describing UI — sorting a list, formatting a date, allocating.
3. Layout or draw running too often. A per-frame value read in the wrong phase. Day 82.
4. Not a Compose problem. Slow app startup, a blocking call on the main thread, an oversized bitmap (Day 60), a database query on the UI dispatcher.
The fourth category is larger than people expect. A janky list is quite often a Room query without an index, and no amount of stability work touches it. It's worth ruling out first precisely because it's the cheapest check and the most commonly skipped.
The diagnostic order
The order matters, because measuring first is what stops the two-week guessing spiral:
First: is it Compose at all? The Android Studio profiler's frame timeline shows what consumed each dropped frame. If the time is in your repository or in image decoding, stop here — Days 79–82 won't help.
Second: which phase? Layout Inspector's recomposition counts answer the composition question directly. A row recomposing 60 times a second while scrolling is a composition problem; one recomposing once while the frame still drops is a layout or draw problem.
Third: why is that phase running? For composition, the compiler report (Day 79) says what can skip and what can't. For layout and draw, it's almost always a value read in the wrong place.
Fourth: was the work necessary? Sometimes the phase is running correctly and the work inside it is simply too much — the category-2 case, fixed by moving work out rather than by reducing invalidations.
The recomposition-count reading
Layout Inspector gives two numbers per composable, and the second is the one people miss:
- Recompositions — how many times the body re-ran.
- Skips — how many times it was invoked and skipped.
High recompositions with zero skips means the composable can't skip — an unstable parameter, which is Day 79. High recompositions with skips means it's being invoked constantly by a parent that's invalidating, which is a problem one level up.
That distinction points at different files, and reading it wrong sends you optimising the wrong component.
What "fast enough" means
Worth fixing before the pillar goes further. A frame budget is 16.67ms at 60Hz, 8.33ms at 120Hz. Missing it occasionally is invisible; missing it consistently during a scroll is what users describe as "laggy".
The metrics that matter, in order of what users notice:
Jank during interaction — dropped frames while scrolling or animating. The most visible, and the one this pillar is mostly about.
Startup time — cold start to first frame. Day 81's subject, and largely not a recomposition problem.
Input latency — the delay between touch and response, which Day 82's deferred reads directly affect.
Chasing recomposition counts on a screen that isn't dropping frames is optimisation without a problem. The counts are a diagnostic, not a score.
Debug builds lie
The single most important caveat in this pillar, and it invalidates most casual measurement: a debug build is not representative.
Debug builds run without R8, without full optimisation, and — crucially — the Compose compiler's live-literals feature makes every literal a state read. Scroll performance in debug can be several times worse than release, and some problems appear only there.
Every measurement in the next five days assumes a release build with R8 enabled, ideally with Baseline Profiles. Anything else measures the build configuration rather than the code.
How to prove it
Establish the baseline before changing anything:
@Test fun scrollJank() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(FrameTimingMetric()),
iterations = 10,
startupMode = StartupMode.WARM,
) {
startActivityAndWait()
device.findObject(By.res("order-list")).fling(Direction.DOWN)
}
Macrobenchmark on a real device reports frame durations at the 50th, 90th, 95th and 99th percentiles. The 99th is where jank lives — a P50 of 6ms with a P99 of 90ms is a list that feels broken despite an excellent average.
Run it, record the numbers, then optimise. Without a before, "it feels faster" is the only evidence you'll have, and it's usually wrong.
The device matters as much as the build. A flagship absorbs inefficiency that a mid-range phone surfaces immediately — Day 60's point about bitmaps applies to composition too. If you benchmark on one device only, make it a slow one; the fast one will tell you everything is fine right up until the reviews say otherwise.
What this generalizes to
The principle is measure the phase, not the symptom. "The list is janky" is a symptom with at least four distinct causes, and the tips that circulate as Compose performance advice are each aimed at one of them.
That's ordinary performance discipline — profile before optimising — with a Compose-specific addition: the profile has to identify which phase, because the fixes are phase-specific and applying the wrong one teaches you nothing.
There's a corollary worth stating early, because it saves the most time: most screens
need none of this. A settings page, a form, a detail view — none of them recompose
enough for any of it to matter. The pillar's techniques earn their complexity in lists,
animations and gesture-driven surfaces, which is a small fraction of an app's screens.
Applying them everywhere is how a codebase acquires @Immutable annotations nobody can
justify. The next five days are the fixes; today is
the part that tells you which to reach for.
Tomorrow, Day 79: stability — what the compiler decides about your types, and how to read what it decided.
Day 78 of a 100-day series on Jetpack Compose, working through the official documentation in order. Source: Compose phases.