block / block/buzz

ResizeObserver feedback loop in the virtualized timeline logs "undelivered notifications" continuously

Open
#5,717 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.