ITADN

[bug]: MessageScroller: rAF-deferred ResizeObserver handling paints every streamed chunk one frame before the scroll correction

#11577Openahardin 创建于 8 天前
A
ahardincommented
### Describe the bug In `@shadcn/react` 0.3.0, the `ResizeObserver` callbacks on `MessageScrollerViewport` and `MessageScrollerContent` defer `handleResize` through `requestAnimationFrame`. (This deferral appears to be the fix for #11084, whose root-cause description shows the callbacks previously invoked `handleResize` synchronously.) The deferral trades the loop warning for a one-frame visual regression: resize observations are delivered before paint in frame N, but the handler now runs in frame N+1's rAF — so frame N **paints with stale scroll state** on every streamed content growth. Concretely, streaming an assistant reply with `autoScroll`: - **Follow mode:** each chunk paints one frame with `scrollTop` short of the bottom by the chunk's height. The scrollbar thumb visibly bounces off the track bottom at chunk cadence. - **Anchored mode (turn anchoring):** the tail spacer is compensated one frame late, so `scrollHeight` wobbles by the chunk height each chunk. The transcript content itself never visibly moves (growth happens below the fold), so the artifact shows only in the scrollbar — invisible on macOS default overlay scrollbars, obvious on classic-scrollbar platforms (Windows, Linux, macOS "Always"). Measured with a paint-accurate probe (a second `ResizeObserver` registered after the library's, so it is delivered later in the same frame's batch): a 60-item streamed reply painted ~170 bad frames. Suggested fix (we run this as a `pnpm patch` in production; happy to PR): run the handler synchronously inside the observer callback where it is loop-safe, and defer only what is not: 1. **Follow path** (`mode === "following-bottom"`): only sets `scrollTop` — cannot resize anything, so it cannot re-trigger the observer. Safe to run synchronously; this alone removes the thumb bounce. 2. **Anchored path**: mutates the spacer (a child of the observed Content), so run it synchronously with the observer suspended around the mutation — `unobserve → reanchor → observe`, where the re-observe is deferred to the next animation frame (calling `observe()` inside the delivery cycle re-triggers the #11084 "undelivered notifications" error), plus a last-content-height memo so the catch-up initial delivery settles instead of ping-ponging. With that change we measure 0 stale painted frames in both modes and 0 `ResizeObserver` loop warnings — i.e. #11084 stays fixed. ### Affected component/components MessageScroller (`@shadcn/react` 0.3.0 — ResizeObserver wiring in Viewport/Content and `handleResize`) ### How to reproduce 1. On a classic-scrollbar platform, stream an assistant reply into `MessageScroller` with `autoScroll` while scrolled to the live edge. 2. Watch the scrollbar thumb: it bounces off the track bottom on every chunk. 3. To measure: create a `ResizeObserver` on the Content element *after* the scroller has mounted (so it is delivered after the library's observer in the same batch) and record `scrollHeight - clientHeight - scrollTop` at each delivery — it reads 20–100px at chunk cadence instead of 0. ### Codesandbox/StackBlitz link https://stackblitz.com/github/ahardin/message-scroller-stream-repro?file=src%2FApp.jsx (Source: https://github.com/ahardin/message-scroller-stream-repro — deterministic fake stream, no LLM/network. The toolbar checkbox toggles the registry `data-autoscrolling:scrollbar-none` class; the on-page counters are the paint-accurate probe described above, so the defect is measurable even on overlay-scrollbar platforms.) ### Logs _No response_ ### System Info ```bash @shadcn/react 0.3.0, Chromium on Arch Linux (classic scrollbars); reproducible on any classic-scrollbar platform ``` ### Before submitting - [X] I've made research efforts and searched the documentation - [X] I've searched for existing issues
1 条评论