Skip to content

Comments

Virtualization supports variable-height items#64964

Open
ilonatommy wants to merge 21 commits intodotnet:mainfrom
ilonatommy:fix-25058-variable-height
Open

Virtualization supports variable-height items#64964
ilonatommy wants to merge 21 commits intodotnet:mainfrom
ilonatommy:fix-25058-variable-height

Conversation

@ilonatommy
Copy link
Member

@ilonatommy ilonatommy commented Jan 7, 2026

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
  1. Dynamic Height Changes After Initial Render - ✅ covered, test added (DynamicContent_ItemHeightChangesUpdateLayout)

  2. Window/Container Resize - ✅ covered, tests added (VariableHeight_ContainerResizeWorks)

  3. Extreme Height Variance - ✅ covered, tests changed to cover x100 variation.

  4. Scroll Position Stability (Scroll Anchoring) - ❌ Out of scope of this PR, for a follow up, see plan.

  5. ItemsProvider with Variable Heights - ✅ covered, async tests added, slow big load tested manually (see the video)

Async-delay.mp4
  1. CSS Transform/Scale on Container - ❌ partially, transform is covered in VariableHeightAsync_CanScrollThroughItems
    We 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.

  2. RTL (Right-to-Left) Layout - ✅ covered in VariableHeightAsync_CanScrollThroughItems

  3. Horizontal Virtualization Interaction ❌ Out of scope

  4. Placeholder Height Mismatch - ✅ covered by the walking average that only initially falls back to ItemSize

  5. Empty or Single-Item Lists - ✅ covered in VariableHeightAsync_SmallItemCountsWork

  6. Items Collection Mutations - ✅ covered, added test VariableHeightAsync_CollectionMutationWorks

  7. Test 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.

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
@ilonatommy ilonatommy force-pushed the fix-25058-variable-height branch from 0b513a2 to c47f845 Compare February 9, 2026 13:19
@ilonatommy ilonatommy marked this pull request as ready for review February 9, 2026 13:20
@ilonatommy ilonatommy requested a review from a team as a code owner February 9, 2026 13:20
Copilot AI review requested due to automatic review settings February 9, 2026 13:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ilonatommy and others added 2 commits February 9, 2026 14:54
…ableHeightAsync.razor

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
ilonatommy and others added 3 commits February 10, 2026 15:59
@ilonatommy
Copy link
Member Author

ilonatommy commented Feb 17, 2026

QuickGrid_SupportsVariableHeightRows failures are caused by #64029.
Source of problem: OnAfterSpacerVisible forces scroll down always when the after-spacer is visible. Even on first render when we go from 0 items loaded to N items, we trigger the scroll.
Fix: The scroll should not happen on first render.

// scrolling glitches.
rangeBetweenSpacers.setStartAfter(spacerBefore);
rangeBetweenSpacers.setEndBefore(spacerAfter);
const spacerSeparation = rangeBetweenSpacers.getBoundingClientRect().height / scaleFactor;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the calculations that are common for both spacers out of the loop to optimize.

@ilonatommy
Copy link
Member Author

ilonatommy commented Feb 19, 2026

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

2 participants