Virtualization supports variable-height items#64964
Virtualization supports variable-height items#64964ilonatommy wants to merge 21 commits intodotnet:mainfrom
Conversation
Implements variable-height support for the Virtualize component by measuring rendered items and using per-item height tracking instead of a fixed ItemSize. Key changes: - JS: Added measureRenderedItems() to measure actual heights of rendered items - JS: Added getCumulativeScaleFactor() for CSS transform handling - JS: Added throttling (50ms) for scroll callbacks to avoid oscillations - C#: Added IVirtualizeJsCallbacks interface for height measurements - C#: Track individual item heights using running average estimation - C#: Clear measurement cache on RefreshDataAsync() - C#: Handle dispose during throttle timeout This enables virtualization to work correctly with items of varying heights, dynamic content changes (accordions, image loading), and RTL layouts. Fixes dotnet#25058
Adds unit tests verifying the Virtualize component correctly handles variable-height items, including: - Height measurement callback processing - Per-item height tracking and averaging - Cache invalidation on refresh
Adds E2E tests covering: - VariableHeight_CanScrollThroughAllItems: Scroll through 100 items with 20-2000px heights - VariableHeight_SpacersAdjustCorrectly: Verify spacer heights update during scroll - VariableHeight_ItemsRenderWithCorrectHeights: Verify items render with specified heights - VariableHeight_ContainerResizeWorks: Test resizing container while scrolled - DynamicContent_ItemHeightChangesUpdateLayout: Test accordion-style expansions - DynamicContent_ExpandingOffScreenItemDoesNotAffectVisibleItems: Scroll stability - VariableHeightAsync_*: Async data loading with variable heights - VariableHeightAsync_CanScrollThroughItems: RTL layouts and CSS transform scale - VariableHeightAsync_CollectionMutationWorks: Add/remove items with height changes - VariableHeightAsync_SmallItemCountsWork: Edge cases (0, 1, 5 items) - DisplayModes_*: Block, Grid, and Subgrid CSS layouts - QuickGrid_SupportsVariableHeightRows: Integration with QuickGrid Also adds test components: - VirtualizationVariableHeight.razor - VirtualizationVariableHeightAsync.razor - VirtualizationDynamicContent.razor - VirtualizationDisplayModes.razor - QuickGridVariableHeightComponent.razor
0b513a2 to
c47f845
Compare
There was a problem hiding this comment.
Pull request overview
This PR extends the Virtualize<TItem> component to support variable-height items by having the JS side measure rendered content and report measurements back to .NET, which then uses a running average height for spacer sizing and item distribution. It also adds multiple new BasicTestApp scenarios plus E2E/unit tests to validate variable-height behavior (including async providers, dynamic content height changes, layout modes, RTL, and transform scaling).
Changes:
- Update Virtualize JS interop to report measured heights of rendered content and throttle spacer callbacks.
- Update
Virtualize<TItem>spacer sizing/distribution math to use a running average of reported heights. - Add new BasicTestApp pages and expand E2E/unit tests to cover variable-height scenarios (including QuickGrid).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Components/Web/src/Virtualization/Virtualize.cs | Uses running average height for spacer sizing/distribution; changes OverscanCount default; processes measurement arrays from JS callbacks. |
| src/Components/Web/src/Virtualization/VirtualizeJsInterop.cs | Extends JS-invokable callbacks to include optional float[] item heights. |
| src/Components/Web/src/Virtualization/IVirtualizeJsCallbacks.cs | Updates internal callback interface signatures to include item height measurements. |
| src/Components/Web.JS/src/Virtualize.ts | Measures rendered element heights, computes scale factor, throttles IntersectionObserver callbacks, and sends measurement arrays to .NET. |
| src/Components/Web/test/Virtualization/VirtualizeTest.cs | Updates existing callback invocation and adds a unit test for accepting measurements. |
| src/Components/test/E2ETest/Tests/VirtualizationTest.cs | Adds E2E coverage for variable-height behavior, dynamic content resizing, supported display modes, and QuickGrid variable-height scenarios. |
| src/Components/test/testassets/BasicTestApp/VirtualizationVariableHeight.razor | New BasicTestApp scenario for extreme height variance + container resizing. |
| src/Components/test/testassets/BasicTestApp/VirtualizationVariableHeightAsync.razor | New BasicTestApp async ItemsProvider scenario with variable heights, RTL, transforms, zoom controls, and mutations. |
| src/Components/test/testassets/BasicTestApp/VirtualizationDynamicContent.razor | New BasicTestApp scenario for height changes after initial render (expand/image-load simulation). |
| src/Components/test/testassets/BasicTestApp/VirtualizationDisplayModes.razor | New BasicTestApp scenario covering supported CSS layout modes (block/grid/subgrid). |
| src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridVariableHeightComponent.razor | New BasicTestApp QuickGrid scenario validating variable-height rows with virtualization. |
| src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor | Sets explicit OverscanCount on existing test scenarios. |
| src/Components/test/testassets/BasicTestApp/VirtualizationTable.razor | Sets explicit OverscanCount for table virtualization scenario. |
| src/Components/test/testassets/BasicTestApp/VirtualizationMaxItemCount.razor | Sets explicit OverscanCount for MaxItemCount scenario. |
| src/Components/test/testassets/BasicTestApp/VirtualizationMaxItemCount_AppContext.razor | Sets explicit OverscanCount for AppContext MaxItemCount scenario. |
| src/Components/test/testassets/BasicTestApp/Index.razor | Adds navigation entries for the new test scenarios. |
src/Components/test/testassets/BasicTestApp/VirtualizationVariableHeightAsync.razor
Outdated
Show resolved
Hide resolved
…ableHeightAsync.razor Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
…ht measurement is done on the whole element as one.
|
|
| // scrolling glitches. | ||
| rangeBetweenSpacers.setStartAfter(spacerBefore); | ||
| rangeBetweenSpacers.setEndBefore(spacerAfter); | ||
| const spacerSeparation = rangeBetweenSpacers.getBoundingClientRect().height / scaleFactor; |
There was a problem hiding this comment.
Moving the calculations that are common for both spacers out of the loop to optimize.
…on jumps by expected vs real height delta.
|
The jump is real for cases with css transformations. We can test it deterministically only on WASM in E2E test. Captured in slow motion with zoom 200%: Jump-Wasm-slow-motion.mp4 |
Design doc:
#65158
Visualization of how the sample works with the initial implementation (scrolling with the scroll & jumps with Ctrl + Home or Ctrl + End):
Working-Variable-Height.mp4
Tested scenarios
Details
Dynamic Height Changes After Initial Render - ✅ covered, test added (
DynamicContent_ItemHeightChangesUpdateLayout)Window/Container Resize - ✅ covered, tests added (
VariableHeight_ContainerResizeWorks)Extreme Height Variance - ✅ covered, tests changed to cover x100 variation.
Scroll Position Stability (Scroll Anchoring) - ❌ Out of scope of this PR, for a follow up, see plan.
ItemsProviderwith Variable Heights - ✅ covered, async tests added, slow big load tested manually (see the video)Async-delay.mp4
CSS Transform/Scale on Container - ❌ partially, transform is covered in
VariableHeightAsync_CanScrollThroughItemsWe have
getCumulativeScaleFactor()but it only checks transform matrix. Other CSS that affects layout (zoom, perspective) are not handled, the are delegated to Fix the bug with scrolling in Virtualize component with scaled elements #64013.RTL (Right-to-Left) Layout - ✅ covered in
VariableHeightAsync_CanScrollThroughItemsHorizontal Virtualization Interaction ❌ Out of scope
Placeholder Height Mismatch - ✅ covered by the walking average that only initially falls back to
ItemSizeEmpty or Single-Item Lists - ✅ covered in
VariableHeightAsync_SmallItemCountsWorkItems Collection Mutations - ✅ covered, added test
VariableHeightAsync_CollectionMutationWorksTest on Multiple Browsers ✅
Tested on Chrome, Firefox, and WebKit — all browsers behave consistently for virtualization: same visible item counts, exact scroll position accuracy, and integer pixel height measurements. The only notable difference is WebKit's 2x device pixel ratio (simulating Retina), which results in one additional sub-pixel item being rendered (22 vs 21).
Fixes #25058,
Fixes #64029,
Fixes #59354.