2b24362cc4f202f6a85c81d438459fc83c30c07e
The same mistake as the draw-phase culling, moved into another phase. Each row held a `derivedStateOf` over the scroll position to decide whether it stayed built. That reads correctly and costs the same shape: every one of those derived states is invalidated by every scroll frame and has to be re-evaluated to learn whether its answer changed, so the per-frame work grew with the number of loaded rows again -- this time landing in the recomposition pass, which the platform reports as the frame's animation phase. On a Pixel 9 Pro XL at 153 rows that was 11.6ms at the median, against an 8.3ms frame, and it was the largest term left. The window is now one range that every row reads, recomputed once a frame from one observer. And recomputed lazily: every row reads it, so every change to it disturbs all of them, which is affordable once every couple of screens and is not affordable at row boundaries, where a fling would cross one every few frames. The window is eight screens either side and it moves in steps of two, so the margin absorbs the staleness. Worth naming as a pattern, since this is the third time: a per-row answer to a question about the scroll position is O(rows) per frame wherever it is evaluated -- in draw, in a derived state, anywhere. The question has one answer and it belongs in one place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Languages
Rust
54%
Kotlin
43.6%
Shell
2.4%