ResizeObserver feedback loop in the virtualized timeline logs "undelivered notifications" continuously
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
**Describe the bug**
`useVirtualizedViewportResize` observes the Virtua scroller and, in a fully
synchronous callback, performs a scroll write that resizes that same element in
the same delivery pass. The browser detects the re-entrancy and logs
`ResizeObserver loop completed with undelivered notifications`.
`desktop/src/features/messages/ui/useVirtualizedViewportResize.ts:27-36`:
```ts
const observer = new ResizeObserver(() => {
if (
shouldSettleVirtualizedViewportResize({
virtualizerAtBottom: virtualizerAtBottomRef.current,
})
) {
settleAtBottom();
}
});
observer.observe(container);
```
Three things combine:
1. The callback takes no `entries`, so it cannot compare sizes. There is no
delta guard — a resize that round-trips to the same layout still settles.
2. There is no `requestAnimationFrame` defer, so the write stays inside the
observation pass.
3. Its only gate, `virtualizerAtBottomRef`, is `React.useRef(true)` at
`useAnchoredScroll.ts:174`. It is armed at rest, before Virtua has reported
anything.
The write path: `settleAtBottom` is `settle` at
`useVirtualizedBottomSettle.ts:122-126`, which calls `cancelFrame()` and then
`pinToBottom()` directly, deliberately bypassing the rAF-throttled
`schedulePinToBottom` at `:50-56`. `pinToBottom` calls
`listRef.current?.scrollToIndex(lastIndex, { align: "end" })`. That shifts
Virtua's rendered range, new rows are measured, the inner sizing element's height
changes and the scrollbar can toggle — resizing the observed element, at a deeper
depth in the same pass.
There is a second observer at `useVirtualizedBottomSettle.ts:116-118` watching
both the content element and the scroller. That one is rAF-deferred so it throws
no error, but it free-runs a frame of work while bottom intent is armed.
**Steps to reproduce**
1. Open a channel with a live, actively streaming timeline. Leave the view at the
bottom.
2. Watch the renderer console.
**Expected behavior**
Staying pinned to the bottom of a live channel should not require a scroll write
inside a ResizeObserver delivery pass, and a resize that does not change geometry
should do nothing at all.
**Version and platform**
- Built from `main`. All three files unchanged since at least `8342dfcc5`;
`useVirtualizedViewportResize.ts` was last modified in #3151.
- Windows 11 Pro 26200.
**Logs / additional context**
```
ResizeObserver loop completed with undelivered notifications
```
Repeated every few seconds, for hours, on an otherwise idle window.
Measured alongside it: the GPU process held **97.1% of one logical core while
visible and 18.5% while minimized**.
That visible-versus-minimized split matches the symptom in #2959. I suspect this
is one contributor there and the `.buzz-shimmer` animation is another; I have
commented on that issue separately with the shimmer measurement. I have **not**
isolated how much of the GPU load belongs to each, so please treat the
attribution as unproven — the mechanism described above is what I am confident
about.
The existing test, `useVirtualizedViewportResize.test.mjs`, only exercises the
pure `shouldSettleVirtualizedViewportResize` predicate. It never mounts the hook
or constructs a `ResizeObserver`, so it cannot see this.
**On a fix.** The shape I would propose is to read `entries[0].contentRect` and
skip when width and height are both within a sub-pixel epsilon of the previous
observed size, treating unknown geometry as changed so the guard fails safe; and
to defer the write by one `requestAnimationFrame`, coalescing and cancelling on
unmount. Not a timer — a `setTimeout` would be a guess at how long layout takes,
whereas a frame boundary is the property that actually matters.
The thing that must not break is that staying pinned to the bottom of a live
channel is the entire point of this machinery. A change that stops the loop by no
longer scrolling would be a regression, so both directions need testing: a new
message with the reader at the bottom still pins, and a resize with
`virtualizerAtBottomRef.current === false` produces no scroll write.
Happy to open a PR if that direction sounds right.
Contributor guide
Research direction
Start with desktop/src/features/messages/ui/useVirtualizedViewportResize.ts and its callers, then read useVirtualizedBottomSettle.ts and useAnchoredScroll.ts to trace observer and pin lifecycles. Run useVirtualizedViewportResize.test.mjs first, then add or execute hook-level coverage for geometry changes, rAF cleanup, bottom, and non-bottom cases. Done means no same-geometry or delivery-pass scroll write, while a new message at bottom still pins.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- desktop, frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100