[Virtualization] Visible content does not shift when in-DOM items above the viewport change height#65951
[Virtualization] Visible content does not shift when in-DOM items above the viewport change height#65951ilonatommy merged 17 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds manual scroll compensation to the Virtualize JS implementation so visible content doesn’t “jump” when in-DOM items above the viewport change height (with native scroll anchoring disabled), and introduces E2E coverage for expand/collapse scenarios.
Changes:
- Track rendered item heights via
ResizeObserverand adjustscrollTopwhen above-viewport items resize. - Always observe rendered items between spacers (not only during convergence) and simplify convergence-related logic.
- Add new E2E tests for expand/collapse-above-viewport stability; adjust the test asset to use more items for true off-screen virtualization.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/Components/Web.JS/src/Virtualize.ts | Implements viewport anchoring via ResizeObserver height-delta tracking and scrollTop compensation. |
| src/Components/test/E2ETest/Tests/VirtualizationTest.cs | Adds E2E tests validating visible-item stability when an above-viewport in-DOM item expands/collapses. |
| src/Components/test/testassets/BasicTestApp/VirtualizationDynamicContent.razor | Increases item count to ensure the scenario exercises real virtualization. |
# Conflicts: # src/Components/test/E2ETest/Tests/VirtualizationTest.cs
After digging a bit deeper, it looks like it's close to being added to stable version. It was added to stable in WebKit/WebKit#58575 but then taken back to preview in WebKit/WebKit@d123c18. We should monitor https://bugs.webkit.org/show_bug.cgi?id=309279. |
If it's on a stable release before November, we can use it. Our support policy is Last Version (-1), so if it lands in Safari 26.x, we are good. |
javiercn
left a comment
There was a problem hiding this comment.
Looks great and very thorough. Great job!
Adds viewport stability to the Virtualize component: visible items stay in place when content above the viewport changes height, and when items are prepended to the collection.
Problem
The Virtualize component disables the browser's native scroll anchoring (
overflow-anchor: none) to prevent an infinite rendering loop. This means any height change above the viewport (item expansion, data updates, etc.) causes visible content to shift ("scroll jumps"). (see issue for more details)Additionally, prepending items to the collection silently shifts what the user sees with no compensation.
Approach: Hybrid scroll anchoring
Uses browser-native CSS Scroll Anchoring where available, with manual JS compensation as fallback.
Why not pure manual compensation like other frameworks?
Every major virtual scroll library (TanStack Virtual, react-window, react-virtualized) uses manual
scrollTopadjustment and requires the developer to handle prepend scenarios. Our hybrid approach gives:Items(zero developer effort)Why native anchoring can't work for
<table>layout?CSS Scroll Anchoring miscalculates positions when
<tr>elements are anchor candidates, causing 3000-8000px jumps per scroll event. Tested 9 CSS approaches — all fail. The fix disables browser anchoring for tables.Description
C# (Virtualize.cs) - prepend detection for
Items:ReferenceEquals+ElementAtOrDefaultfor O(1) access)._itemsBeforesospacerBeforegrows, creating a real DOM shift that the browser (or manual path) compensates automatically.DefaultItemsProvider- customItemsProviderreturns new instances per request, making reference comparison unreliable. We would have to add new API for developers to inform virtualization component that they prepended. Other frameworks either do not support it at all (TanStack) or have additional manual step that is considered "convoluted" even by the maintainers and it still causes table jumps (e.g.react-virtuosoand itsfirstItemIndex;react-windowwithscrollOffsetadjustments;react-virtualizedthat requires callingscrollToPosition()).JS (Virtualize.ts) - two compensation mechanisms:
overflow-anchor: noneonly on spacers. The browser anchors on rendered items and compensatesscrollTopatomically during layout.ResizeObserver; compensatescrollTopwhen items above the viewport resize.Manual path details:
scrollTopwhen the resized item is entirely above the viewport.Convergence changes:
End/Homekeydown listener starts convergence immediately, before the browser updates scroll position. With native anchoring enabled the browser may preventscrollTopfrom reaching the absolute extremity, so IO-based detection alone (onSpacerAfterVisible) is not sufficient.IntersectionObservercallbacks are throttled via a pending-entries map to deduplicate rapid intersection events.expansion-above-viewport.mp4
Fixes #65939