Repository navigation
🤖 perf: hold the first row in view while scrolled up, before paint (#6773) - #6959
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 714d503afc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
|
Perf owner: #6959 is READY on review (Codex clean on
|
|
Perf owner: quiet-host rerun results (lane scripts, run by me):
|
|
Perf owner: interleaved th20k block (ABBA, 3+3) result: main max press 155 / 235 / 111 ms (0 runs over 250); head 122 / 119 / 317. The 317 ms head run is disturbed: the queue's quiet check passed at lock time, but the run's start line shows load 27.92 and CPU PSI 19.57 (host load rose to ~31 during it), against the declared quiet-host precondition (load < 10, PSI < 10). I am not dropping it by judgment: I replace that one run with a single head th20k rerun under the same precondition and the same rule (head runs with a press > 250 ms must not outnumber main's). Replacement result decides. |
|
Perf owner readiness record: QUEUED at |
|
Perf owner: DROPPED 23:06Z (merge-group run 39174280603, Test / Storybook): |
Summary
While the reader is scrolled up, a row whose top is above the view and that grows (KaTeX arriving late, a lazy Mermaid diagram, a late image) no longer pushes the rows below it down.
useAutoScrollnow keeps its ResizeObserver connected while released and holds the first row that starts inside the view at its on-screen top, before paint. Fixes #6773 (F20 part 2b-2b-2, #4961).Background
Chrome anchors to the visible part of a partly visible top row. Growth below that anchor, inside the same row, is outside what native scroll anchoring compensates, so every row below moved down by the growth. On main the observer that re-pins the bottom disconnected while scrolled up, so no code reacted.
Implementation
useReadingRowHold(inuseAutoScroll.ts):snapshotReadingRowrecords the first[data-message-id]row whose top is inside the scrollport (binary search, O(log n) rect reads) and its top.holdReadingRowmovesscrollTopby however far that row moved, then records again.stickToBottom()while locked (unchanged) andholdReadingRow()while released. Snapshots are taken on every scroll event while released and ondisableAutoScroll.Validation
Tests first (first commit):
tests/ui/chat/partialTopRowGrowth.test.tsgrows the partly visible top row by 397 px and fails on main (the next row moved: expected 44, received 441). Its control (growth of the first row in view) passes on both. Two hook controls inuseAutoScroll.test.tsx: rows mounted above with the caller's anchor correction (Load older) move nothing more, and with no row starting in view the hook writes nothing. happy-dom has no layout, so the tests use a layout model; the browser proof is below.Browser proof (release builds, headless Chrome 155,
xum server, a synthetic 30-row demo chat): scrolled up (wheel) so a row shows only its bottom 44 px at the top.<div>inserted into the row<div>inserted into the rowHead after the 397 px insert (1800 px): the inserted block sits above the view and "Demo question 18" stays put.
Main after the delayed KaTeX upgrade (1800 px): the upgraded math row pushed the rows below it down by 215 px.
Head after the same upgrade (1800 px): the rows below stay put.
React Compiler:
useAutoScrollanduseReadingRowHoldboth compile (CompileSuccess, no CompileError).make check-react-compiler23/24, the same as main.make static-check,make crap-check(PASS; the hold is its own hook, souseAutoScroll's CC does not grow),useAutoScroll.test.tsx(29 pass),tests/ui/chat/{partialTopRowGrowth,composerDockAnchoring,bottomLayoutShift}.test.ts(9 pass).Perf gates (local host, every run started at load1 < 10, one run per hold of the shared measurement lock; main = 83a8168, head = edbceef, whose product code is the same as 7900d92):
f20 Load-older harness, 40 presses per run, main and head alternating (ABBAAB): 21 of 24 runs done. 1240k has 1 main and 2 head runs; the host stayed busy (load 40-95) after 18:50 UTC.
Head: 1 of 320 presses over 200 ms (th20k press 38 of one run, a reveal at 361 ms). Main: 5 of 280 (one 100k run had 4, at 206-401 ms; one th20k press at 208 ms). Heap: 198 MB or less on both sides (limit 250 MB). Mounted rows end at 1,198-1,199 on both sides.
perf.chatSwitch + perf.workspaceOpen (Electron, 3 runs per side, ABBAAB): head medians are within run noise. They are equal or faster except workspace-open-large (data-loaded 775 -> 837 ms, run ranges 752-861 vs 739-846, overlapping) and chat-switch switch-back-small in-process caughtUp (55 -> 66 ms, non-overlapping runs). Neither path runs the hold: the transcript stays locked on open and on switch. The full table is in the lane evidence (e2e-table.md).
Risks
Medium-low, scroll behavior while scrolled up. While released, each ResizeObserver delivery and scroll event now does a
querySelectorAllplus about log2(rows) rect reads. A wrong delta would move the reader's view; the hold is skipped when the held row is gone and re-snapshots after every scroll.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:high• Cost:$8.95