Agent chat: smoother transcript scrolling
Leo-ask: ask-b20c21f8c8
i also feel like me just scrolling through the agent chat isn't as smooth and buttery as it could be... it's all a bit slow rn cuz my machine is under load but i think there are still random hitches?
Cause. The transcript keeps a "warm" window of rows (one viewport either side of the reader) mounted in a deferred render, so rows a scroll brings in are already drawn. The window was driven by useDeferredValue(scrollTop). Because the offset changes every pixel, every scroll frame scheduled a second render of the whole VirtualTranscript, and the next scroll event often restarted it. Those renders drew the same warm rows again. On a loaded machine this extra main-thread work is where random hitches come from.
Fix. The deferred offset moves in half-viewport steps (WARM_STEP_VIEWPORTS = 0.5). At least ¾ of a viewport (plus overscan) stays warm on each side. The rows in view and the scroll-lead rows come from the live offset as before, so this can't leave the view unmounted.
Bench. webviews/bench/transcript-scroll-frame.bench.ts is new. It runs a 3000-row chat in headless WebKit through the transcript-fling harness and scrolls it two ways: wheel steps of 40 px a frame (a reading scroll), and the debug fling (startFling). For each, it prints frame intervals and the transcript's own per-frame work from acpmuxPerf.
Performance
Wheel scroll, 3 rounds × 480 frames, on a fast-checks Linux runner (8 vCPU). Values are p50/p95/max per frame.
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| VirtualTranscript renders / frame | 3 / 4 / 4–5 | 2 / 2 / 4 | −1 p50, −2 p95 | bench, before → after |
| React ms / frame | 1 / 4 / 5–7 | 1 / 1 / 7–8 | p95 −3 ms | same |
| row renders / frame (max) | 52 (round 0) | 10 | −42 | same |
| row.measure ms / frame (p95) | 2 | 0 | −2 ms | same |
| frame interval ms | 21 / 22 / 23–39 | 21 / 22 / 23–24 | max −15 ms (round 0) | same (the interval is paced by the harness's one Playwright step per frame) |
Fast flings (≈5 viewports a frame) are dominated by mounting new rows, which this change doesn't touch. Their numbers vary run to run on the runner (blank frames 6–27 of 31–72 after, 11 of 52 in the one baseline round the log kept). transcript-fling.test.ts remains the guard for coverage (≤5% blank frames).
Timers, observers and loops: none added. The bench's rAF loop stops on its __wheelRun flag, and the page closes at the end.
Follow-ups (from review): add hysteresis at the half-step boundary for a reader resting on it.
Agent session: agent session
🤖 Generated with Claude Code