shift anchoring can get stuck in a requestAnimationFrame loop and freeze the scroller
Describe the bug
With shift on, alignShiftAnchor() can keep rescheduling itself every frame and never stop. When that happens the scroller stops responding to scrolling at all.
setScrollTop() writes the target to el.scrollTop without clamping it:
if (!scrollerEl || Math.abs(scrollerEl.scrollTop - target) < 0.5) {
return false
}
_applyingShiftAnchor = true
scrollerEl.scrollTop = targetBrowsers clamp out of range writes silently. So if the target is bigger than scrollHeight - clientHeight, scrollTop never gets there. The < 0.5 check stays false, setScrollTop keeps returning true, stableFrames never hits 2, and the loop never ends.
The bad part is _applyingShiftAnchor. It gets set on every iteration, and onNativeScroll throws away every scroll event while it's true, including the user's own wheel. So clearShiftAnchor() never runs and you can't scroll your way out. Only a reload fixes it.
I ran into this in a chat feed that prepends older messages. While it was stuck:
- the container was still scrollable:
scrollTop 3982, scrollHeight 5007, clientHeight 1025 el.scrollTop = 0worked for a moment, then it was back at 3982 (the max)- I patched the
scrollTopsetter and logged it: 600+ writes of4478, same stack every time (rAF -> alignShiftAnchor -> setScrollTop), still counting up. 4478 is past the max of 3982.
3.0.5 has the same code.
Looks like the same kind of loop as #221, but on the shift path.
I have a fix ready (clamp the target before comparing and writing) plus a test. Happy to open a PR.
I used an AI agent to help dig into this. The numbers above are from real runs, not guesses.
Reproduction
StackBlitz: https://stackblitz.com/github/aqu1les/vue-virtual-scroller/tree/repro-shift-anchor-freeze
Click "Reproduce the freeze", then try to scroll the list with the wheel. Nothing moves.
The button does what my chat feed does when you open a conversation that has no scroll and pagination fires right away:
- scroll to the bottom of a 40 message conversation (200px rows)
- swap the items for a 3 message conversation
- next tick, prepend 20 older messages (40px rows)
The counter above the list tracks writes to scrollTop. On 3.0.5 it goes up about 60 times a second and never stops, and the last written value (800) is above the max (320). With the clamp it stops after 12 writes and the wheel works again. Same result on 5 runs.
One thing that surprised me: a plain prepend can't cause this. target and scrollHeight - clientHeight grow by the same amount. You need the anchor captured while scrollTop still belongs to the previous, taller list, so logicalOffset comes out way too big. That's probably why it felt random in my app.
System Info
System:
OS: Linux 5.15 Ubuntu 22.04.5 LTS 22.04.5 LTS (Jammy Jellyfish)
CPU: (12) x64 11th Gen Intel(R) Core(TM) i5-11400H @ 2.70GHz
Memory: 7.05 GB / 23.47 GB
Container: Yes
Shell: 5.8.1 - /usr/bin/zsh
Binaries:
Node: 22.19.0
Yarn: 1.22.22
npm: 10.9.3
pnpm: 10.6.5
bun: 1.4.0
Browsers:
Chrome: 139.0.7258.127
npmPackages:
vue: 3.5.13
vue-virtual-scroller: 3.0.4 (same code in 3.0.5)Used Package Manager
pnpm
Validations
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Check that this is a concrete bug. For Q&A open a GitHub Discussion.
- The provided reproduction is a minimal reproducible example of the bug.
Source: Akryum/vue-virtual-scroller