Compose performance and measurement
Diagnose the frame, not a guess
A frame may spend time in composition, layout, drawing, image decoding, GC, binder calls, or unrelated main-thread work. “Too many recompositions” is not a diagnosis.
Use a release-like build and a repeatable user journey. Debug builds distort Compose performance.
Place state reads in the latest phase
// May recompose as offset changes.
Modifier.offset(x = offsetDp, y = 0.dp)
// Lambda reads during placement and can avoid composition for each change.
Modifier.offset { IntOffset(offsetPx(), 0) }
Similarly, a draw lambda can read rapidly changing visual state during drawing. This is useful only when semantics/layout do not depend on the same value.
Lazy list checklist
- stable keys and
contentType - no sorting/filtering inside
items - expensive formatting moved out or remembered with correct keys
- thumbnails sized before decode and cached by image library
- nested same-direction scrolling avoided
- item models immutable and callbacks stable enough for measured needs
val visibleProducts = remember(products, filter) {
products.filter(filter::accepts)
}
LazyColumn {
items(visibleProducts, key = Product::id, contentType = { "product" }) { ProductRow(it) }
}
Stability is not the first lever
Strong skipping and compiler inference can reduce work, but annotation lies are dangerous. Before changing stability:
- Capture system trace or Macrobenchmark evidence.
- Inspect recomposition/high-cost scopes and compiler reports.
- Verify model mutation contracts.
- Make the smallest truthful change.
- compare distribution (median and tail), not one run.
Measure user outcomes
- Macrobenchmark: startup, scroll, navigation journeys across process boundaries.
- JankStats/system tracing: slow frames and causes.
- Baseline Profiles: precompile critical paths.
- Microbenchmark: isolated algorithm/layout work, not full-screen claims.
Screenshot smoothness in an emulator is weak evidence. Benchmark representative release artifacts on physical devices, including lower-end hardware.
Moving all work to Dispatchers.Default does not make a screen fast. Results still arrive, allocation/GC remains, and unsafe parallelism may create races. Remove unnecessary work and assign thread ownership deliberately.
Sources
Interview exercises
- Investigate a janky product list without assuming recomposition is the cause.
- Why are debug-build timings misleading?
- Distinguish Macrobenchmark, Microbenchmark, and Baseline Profiles.