f9432b69cce09a9728c568ece57d792cc8e1592a
Iris's Pixel 9 Pro XL said which half was costing, and it was not the half either of us was looking at. With 138 rows loaded: layout 0.0ms at the median, GPU 1.9ms, and **draw 13.6ms** against a 120Hz budget of 8.3ms. Nothing was being re-measured and the phone's rasteriser was idle; the UI thread was recording draw commands for a transcript that was almost entirely off screen. Retaining rows was right and stays. What does not follow the same rule is drawing: the draw pass walks the whole tree, so a list that keeps every row alive records every row every frame, and that cost grows with each page of history -- which is exactly what "worse afterwards" was. Composition and measurement are what must not be thrown away, because they are what has to be rebuilt from nothing when the reader comes back. A display list is rebuilt from a layout that is still there. So each row skips its own draw when it is off screen, by more than a screen's margin either side. The check reads the scroll position from the draw phase, so moving the list invalidates drawing and nothing else, and it is a lookup rather than a sum -- the running totals are rebuilt at most once a frame and only after something has actually changed height, since adding them up per row per lookup would have made the fix quadratic in the thing it was fixing. Also reports the three frame phases that were missing, which is why the phases on that reading did not add up to the total: the time a frame spent waiting for the UI thread to be free, handling input, and running animations. About 15ms of the 30.6 was in that gap and unattributed. On the emulator, same transcript, before and after: draw p50 3.9ms -> 2.5ms, p90 7.3ms -> 4.2ms, p99 48.6ms -> 5.2ms. The saving there is small because only 60 rows were loaded; it is proportional to how much is off screen, and on the phone that was 95,000px of content against a 1,474px viewport. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Languages
Rust
54%
Kotlin
43.6%
Shell
2.4%