Zygote
Builder trackintermediate

B05 — Janky list, and the fix

Build a list that janks on purpose, measure it, then fix it three ways and measure again. A before-and-after you can reproduce on your own device, with the trace to prove it.

7 minAPI 21–37 · 17interactiveverified 2026-10-27

Performance advice is easy to repeat and hard to believe. This project makes the belief cheap: you build a list that janks, you measure it, you fix it, and you measure the same thing again.

What you are building#

A LazyColumn of rows that do expensive work during composition: parse a string, allocate a list, and do a little formatting. A second screen shows the fixed version, which moves the work off the composition path and caches the result. A control switches between them so the comparison is one tap.

Steps#

  1. Create the project and the slow list

    Build a LazyColumn with a few hundred rows. In each row, do the expensive work inline: allocate a large list, format a date with a new formatter, and compute a derived value. Do it inside the composable body so it runs on every composition.1

    You should seeA scrollable list that visibly stutters.
  2. The slow row does its work in the composable body:

    @Composable
    fun SlowRow(item: Item) {
        // Runs on every composition of this row.
        val formatted = expensiveFormat(item)
        Text(text = formatted)
    }

    The fix hoists it out of the composition path:

    @Composable
    fun FastRow(item: Item) {
        val formatted = remember(item.id) { expensiveFormat(item) }
        Text(text = formatted)
    }
  3. Make the jank reproducible

    Turn off animations and use a fixed scroll distance, for example via adb shell input swipe with the same coordinates. Reproducibility is what makes the measurement meaningful.

    You should seeThe same scroll produces the same stutter.
  4. Measure with gfxinfo

    Reset the counters, scroll, and read the summary. Record the number; it is your baseline.

    You should seeA jank percentage above zero.
  5. Record a trace

    Record with the lab 10 config while scrolling, then find a janky frame and confirm that the main thread slice is the cause.

    You should seeA janky frame in FrameTimeline.
  6. Fix one: hoist the work

    Move the expensive computation out of the composable body and into a remember keyed on the row's stable inputs, or into the ViewModel. The work happens once per item instead of once per recomposition.

    You should seeThe jank percentage drops.
  7. Fix two: stable keys and types

    Give the list stable keys and make the row's data class stable so Compose can skip unchanged items. An unstable type forces recomposition even when the values are equal.

    You should seeFewer recompositions of unchanged rows.
  8. Fix three: add a baseline profile

    Generate and install a baseline profile so the hot code is compiled ahead of time. The gain is usually smaller than the structural fixes, which is itself a useful lesson about ordering.

    You should seeA smaller improvement, concentrated at startup and first scroll.
  9. Measure again

    Repeat the exact same scroll and read gfxinfo. Compare the two numbers and the two traces.

    You should seeA clearly lower jank percentage than the baseline.
Measure, then measure again
adb shell dumpsys gfxinfo com.example.jankylist reset
adb shell input swipe 540 1600 540 400 300
adb shell dumpsys gfxinfo com.example.jankylist
adb shell dumpsys gfxinfo com.example.jankylist framestats | head -40
Example output
Illustrative output — numbers differ per device.

Verification#

The comparison is the verification. You should be able to show a before number, an after number, and a trace for each that attributes the difference to a specific change. If the after number is noise, the scroll was not reproducible or the device was doing something else.

Loading diagram…
Text description of this diagram

An annotated timeline with the app main thread, the RenderThread, SurfaceFlinger and two CPU lanes. The main thread row shows a long activity creation slice followed by a first draw; the RenderThread row shows its own work starting after the main thread hands over; the SurfaceFlinger row shows composition and presentation. The point for this project is that the list's expensive composition appears as a wide main-thread slice, and the fix reduces that slice rather than the RenderThread work.

What you learned#

  • Jank is a missed deadline, not a feeling. It is measurable with gfxinfo and attributable with a trace.
  • The order of fixes matters. Removing work from the composition path usually beats compiler optimisation; the compiler helps the code that remains.
  • Stability and keys are about skipping work, not about correctness. They reduce recomposition without changing what is drawn.
  • A baseline profile is not a substitute for structure. It makes the hot path faster; it does not make a slow path disappear.

Read Jank, measured for the frame model, and Lab 12 — Jank for the trace analysis.

Extend it#

  1. Add a second slow path that runs on the main thread after composition, such as an image decode, and compare its cost with the composition cost.
  2. Replace the Compose list with a RecyclerView and compare the jank profiles; the two toolkits fail differently.
  3. Add a Macrobenchmark that runs the scroll in a test and asserts on frame timing, so the comparison becomes a regression test rather than a manual ritual.

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsCompose performancedeveloper.android.comaccessed 2026-10-27↩
  2. docsการแสดงผลช้า | App qualitydeveloper.android.comaccessed 2026-10-27↩
  3. docsMacrobenchmark overviewdeveloper.android.comaccessed 2026-10-27↩
  4. docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
  5. docsBaseline Profiles overviewdeveloper.android.comaccessed 2026-10-27↩
  6. docsProfile Guided Optimizationdeveloper.android.comaccessed 2026-10-27↩
  7. docsInspect GPU rendering speeddeveloper.android.comaccessed 2026-10-27↩