feat(virtual-core): add skipRemeasurementOnBackwardScroll option to reduce stuttering
🎯 Changes
- problem
- During heavy scrolling with dynamic item sizes, frequent scroll adjustments can cause:
- Layout thrashing due to multiple synchronous scroll position updates
- Stuttering during backward scrolling because already-measured items are remeasured unnecessarily
- Related issue: #659
- Solution
-
Batching Scroll Adjustments
- Collect multiple adjustment deltas in
pendingAdjustmentDeltasarray - Use
requestAnimationFrameto batch-apply all adjustments in a single frame - Reduces layout thrashing and improves scrolling performance
- Collect multiple adjustment deltas in
-
Backward Scroll Optimization
- Add
disableScrollAdjustmentOnBackwardScrolloption - Skip remeasuring items that are already in cache during backward scroll
- Prevents stuttering while maintaining measurement accuracy
- Add
✅ Checklist
- [x] I have followed the steps in the Contributing guide.
- [x] I have tested this code locally with
pnpm run test:pr.
🚀 Release Impact
- [x] This change affects published code, and I have generated a changeset.
- [x] This change is docs/CI/dev-only (no release).
🦋 Changeset detected
Latest commit: d284d4fb579348732e81d234af959e01e3e607e0
The changes in this PR will be included in the next version bump.
This PR includes changesets to release 7 packages
| Name | Type |
|---|---|
| @tanstack/virtual-core | Minor |
| @tanstack/angular-virtual | Patch |
| @tanstack/lit-virtual | Patch |
| @tanstack/react-virtual | Patch |
| @tanstack/solid-virtual | Patch |
| @tanstack/svelte-virtual | Patch |
| @tanstack/vue-virtual | Patch |
Not sure what this means? Click here to learn what changesets are.
Click here if you're a maintainer who wants to add another changeset to this PR
@dpwls02142 Thanks for the PR, this is addressing a very real problem. I think there are actually two distinct changes here:
- Skipping re-measurement when scrolling backward
- Batching scroll adjustments via requestAnimationFrame
I’d strongly suggest splitting these into two separate PRs:
-
Backward scroll optimization The option name reads like it disables scroll adjustment, but in reality it skips measurement under certain conditions. It might be worth either renaming it or being very explicit about this behavior in the docs. There’s also an open question around how we guarantee that the layout eventually settles correctly after scrolling stops (i.e. when isScrolling flips back to false).
-
Scroll adjustment batching There is no scroll adjustment batching implemented as by designed as when scrolling quickly, you see more visible white space. Because the scroll adjustments are delayed to the next requestAnimationFrame, the content height changes while the scroll offset isn’t corrected immediately, so during fast scrolling you get gaps until the batch flushes. The faster you scroll, the more noticeable this becomes. This isn’t a trivial issue to solve, it’s the fundamental trade-off between less thrashing vs. more lag. With dynamic heights and fast scrolling, you really feel that lag.
@dpwls02142 Thanks for the PR, this is addressing a very real problem. I think there are actually two distinct changes here:
- Skipping re-measurement when scrolling backward
- Batching scroll adjustments via requestAnimationFrame
I’d strongly suggest splitting these into two separate PRs:
- Backward scroll optimization The option name reads like it disables scroll adjustment, but in reality it skips measurement under certain conditions. It might be worth either renaming it or being very explicit about this behavior in the docs. There’s also an open question around how we guarantee that the layout eventually settles correctly after scrolling stops (i.e. when isScrolling flips back to false).
- Scroll adjustment batching There is no scroll adjustment batching implemented as by designed as when scrolling quickly, you see more visible white space. Because the scroll adjustments are delayed to the next requestAnimationFrame, the content height changes while the scroll offset isn’t corrected immediately, so during fast scrolling you get gaps until the batch flushes. The faster you scroll, the more noticeable this becomes. This isn’t a trivial issue to solve, it’s the fundamental trade-off between less thrashing vs. more lag. With dynamic heights and fast scrolling, you really feel that lag.
Hi @piecyk, thanks for the detailed feedback!
I agree with splitting this into two separate PRs. I'll focus on the backward scroll optimization and remove the batching approach.
Changes
- Removed all batching code (
pendingAdjustmentDeltas,scheduleAdjustmentBatch,flushAdjustments,adjustmentBatchTimer) - Renamed to
skipRemeasurementOnBackwardScrollto clarify it's skipping measurement, not adjustment
About layout stability You're right to ask about this. My thinking is that by the time a user scrolls backward, they've already scrolled forward past those items, so the measurements were already captured during the initial forward scroll. We're just reusing those cached values during the backward scroll to avoid the stuttering.
If an item's size genuinely changed while off-screen, it would get picked up the next time it enters the viewport during normal (non-backward) scrolling.
Let me know if you see any issues with this approach!