Skip to content

Fix  InitialItemIndex viewport underfill for small items in big container or on window resize - #67936

Merged
ilonatommy merged 13 commits into
mainfrom
fix-67933-small-initial-index
Aug 21, 2026
Merged

Fix  InitialItemIndex viewport underfill for small items in big container or on window resize#67936
ilonatommy merged 13 commits into
mainfrom
fix-67933-small-initial-index

Conversation

@ilonatommy

@ilonatommy ilonatommy commented Jul 21, 2026

Copy link
Copy Markdown
Member

Fixes #67933

Problem

Virtualize<TItem> lets you load a list form a given InitialItemIndex instead of index 0. The component seeds a window of items around that index using a rough guess of item height, then relies on the browser's IntersectionObserver/spacer callbacks to detect whether the viewport is actually filled and grow the window if not.

The bug: once the initial guess placed the window near the end of the list (or items were smaller than assumed, or the container was unusually tall/resized), the "did we fill the viewport" check could be satisfied or short-circuited prematurely. The result: only a small strip of real rows rendered, with visible empty gaps above and/or below the viewport that a user scroll (not a resize/re-render) was needed to fix.

Fix

AlignToItemAsync now returns the post-alignment viewport state: covered, before spacer visible, or after spacer visible. Virtualize<TItem> uses that browser-measured result to grow the rendered window geometrically in the required direction while preserving target alignment.

Initial-index positioning repeats this align-and-grow cycle until real content covers the viewport or no further growth is possible. ScrollToItemAsync performs the same immediate viewport-growth check after scrolling, with subsequent filling handled by the existing spacer observers. Growth remains bounded by the list size and MaxItemCount plus overscan.

viewport-fill.mp4

@ilonatommy ilonatommy added this to the 11.0-preview7 milestone Jul 21, 2026
@ilonatommy ilonatommy self-assigned this Jul 21, 2026
@ilonatommy
ilonatommy requested a review from a team as a code owner July 21, 2026 12:20
Copilot AI review requested due to automatic review settings July 21, 2026 12:20
@ilonatommy ilonatommy added area-blazor Includes: Blazor, Razor Components feature-blazor-virtualization This issue is related to the Blazor Virtualize component labels Jul 21, 2026
@ilonatommy
ilonatommy temporarily deployed to copilot-pat-pool July 21, 2026 12:21 — with GitHub Actions Inactive
@ilonatommy
ilonatommy temporarily deployed to copilot-pat-pool July 21, 2026 12:21 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 fixes a Blazor Virtualize/QuickGrid startup issue where small InitialItemIndex values could leave the viewport bottom underfilled until the user scrolls. It does this by deferring “leading overscan” (items reserved above the target) until the component has received its first spacer/measurement feedback, and adds E2E coverage to prevent regressions.

Changes:

  • Add a _hasSpacerFeedback flag to detect when initial spacer observer feedback has occurred and use it to avoid reserving pre-target overscan during the initial unmeasured window seed.
  • Mark spacer feedback as received when OnBeforeSpacerVisible/OnAfterSpacerVisible run, restoring the normal overscan-before-target behavior after measurement.
  • Add E2E tests asserting that both QuickGrid and Virtualize fill the viewport for small InitialItemIndex values (including a small overscan scenario), plus minor test-asset updates to support the scenario.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/Components/Web/src/Virtualization/Virtualize.cs Defers leading overscan until spacer feedback exists to prevent initial viewport underfill.
src/Components/test/testassets/BasicTestApp/VirtualizationAnchorMode.razor Adds a UI toggle to set a small overscan to reproduce/validate the scenario.
src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridScrollComponent.razor Adjusts rendered item styling in the scroll/initial-index test component used by E2E coverage.
src/Components/test/E2ETest/Tests/VirtualizationTest.cs Adds E2E assertions ensuring initial-index loads cover the viewport bottom with real items for both QuickGrid and Virtualize.

@ilonatommy ilonatommy modified the milestones: 11.0-preview7, 11.0-rc1 Jul 22, 2026
@ilonatommy ilonatommy changed the title Fix  InitialItemIndex viewport underfill for small indices Fix  InitialItemIndex viewport underfill for small items in big container Jul 30, 2026
@ilonatommy
ilonatommy requested a review from maraf July 30, 2026 14:22

@MayaKirova MayaKirova left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure if this is something that needs an outsider's review, but I did spend some time looking into the Virtualization Component (partly out of curiosity because we do also have virtualization in the IgniteUI components) so decided to give it a go. :)

Comment thread src/Components/Web.JS/src/Virtualize.ts Outdated
Comment thread src/Components/test/testassets/BasicTestApp/VirtualizationAnchorMode.razor Outdated
Comment thread src/Components/Web/src/Virtualization/Virtualize.cs Outdated
@ilonatommy

ilonatommy commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Thank you for the review. I will draft it now since this PR will wait for #68114 to get in first - this PR changes are stacked on 68114.

@ilonatommy
ilonatommy marked this pull request as draft August 6, 2026 09:10
@ilonatommy
ilonatommy force-pushed the fix-67933-small-initial-index branch 2 times, most recently from fb04ab7 to 79dfd31 Compare August 6, 2026 10:46
@ilonatommy ilonatommy changed the title Fix  InitialItemIndex viewport underfill for small items in big container Fix  InitialItemIndex viewport underfill for small items in big container or on resize Aug 10, 2026
@ilonatommy ilonatommy changed the title Fix  InitialItemIndex viewport underfill for small items in big container or on resize Fix  InitialItemIndex viewport underfill for small items in big container or on window resize Aug 10, 2026
@ilonatommy
ilonatommy force-pushed the fix-67933-small-initial-index branch from 79dfd31 to 4b76966 Compare August 13, 2026 13:29
@ilonatommy
ilonatommy force-pushed the fix-67933-small-initial-index branch from 4b76966 to a59b5d4 Compare August 14, 2026 06:24
@ilonatommy
ilonatommy force-pushed the fix-67933-small-initial-index branch from 180bfd1 to cb511d9 Compare August 14, 2026 14:00
Comment thread src/Components/Web/src/Virtualization/Virtualize.cs Outdated
Comment thread src/Components/Web/src/Virtualization/Virtualize.cs Outdated
@PureWeen

PureWeen commented Aug 19, 2026

Copy link
Copy Markdown
Member

Disclosure: This review was produced with assistance from GitHub Copilot. The browser reproduction and instrumentation described below were run against the current PR head before posting.

I found a near-end case that still underfills on the current head (8ffabe54).

Using the real Interactive Server browser path in Chrome (not synthetic callback injection):

  • 2,000 items
  • ItemSize=50
  • default OverscanCount=15 and MaxItemCount=100
  • 2,500px-tall container
  • InitialItemIndex=1990

The provider is called once with Count=31, only items 1975-1999 render, and roughly half the viewport remains blank above them. Both spacer elements end up geometrically inside the viewport, the bottom is covered, and the top is not. The matched InitialItemIndex=1950 control works: it requests 31,82, renders 1918-1999, and covers both edges.

The trace does not show the same-batch bothSpacersIntersect guard causing this. The before-spacer callback occurs during programmatic scrolling and is ignored while _currentScrollCts is active. Scrolling then clamps at the end, so the already-intersecting before spacer does not produce another transition. The after spacer has zero height and its callback is skipped. The window therefore never grows or shifts backward.

Can we extend the fix so that when initial positioning clamps near the end and forward growth cannot fill the viewport, it expands or shifts the window backward while keeping the target visible?

I think the regression coverage should include the 2,000-item/index-1990 case paired with index 1950 as a positive control, asserting that both viewport edges are covered without user scrolling and that requests terminate. It would also be worth covering both ItemsProvider and in-memory Items, plus QuickGrid if it shares this path.

@kotlarmilos kotlarmilos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, thanks!

@PureWeen

Copy link
Copy Markdown
Member

Disclosure: This review was produced with assistance from GitHub Copilot. The browser reproduction and instrumentation below were run against the current PR head, ab1e067c.

Thanks for iterating on this. The latest change fixes the original initial-load underfill in my exact 2,000-item / InitialItemIndex=1990 browser repro. It now requests 31,62, renders through item 1999, and covers both viewport edges.

I did find a deterministic resize regression on the same path though. With fixed 50px items and a 2,500px container, the first visible item starts at 1950. After resizing the container to 2,000px and back to 2,500px, it moves to 1362 and stays there. The index-1950 control similarly moves to 1751. On the previous head, that control stayed at 1950 through both resizes.

The instrumented trace shows an out-of-order rendered-content measurement:

  1. JS measures 25 rendered items with a 1,250px separation.
  2. Pending growth commits a 62-item window before that fire-and-forget callback reaches .NET.
  3. .NET consumes the old 1,250px value while _lastRenderedItemCount is already 62.
  4. RecalibrateItemSize changes _itemSize from 50 to 20.161291, and the effective measured height becomes 28.735632.
  5. The next correct 62-item / 3,100px measurement only brings the polluted running average to 37.583893. The before spacer is then about 55,689px instead of roughly 96,900px, so the next resize redistributes to the wrong part of the list.

Can we preserve the rendered-window identity with the measurement, or otherwise reject/supersede a measurement once the rendered slice has changed? I tried awaiting the measurement as a diagnostic, but tiny floating-point changes caused repeated alignment work, so I don't think that is the right production fix by itself.

I think the regression assertion should verify that the initial anchor remains stable through resize smaller and larger, not only that both viewport edges are covered. It should keep the 1990 case paired with the 1950 control and cover Items, ItemsProvider, and QuickGrid. The current Mono Server QuickGrid in-memory case is also red in build 1561049, so that should stay part of the validation rather than weakening its item-950 assertion.

@ilonatommy

ilonatommy commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

I did find a deterministic resize regression on the same path though. With fixed 50px items and a 2,500px container, the first visible item starts at 1950. After resizing the container to 2,000px and back to 2,500px, it moves to 1362 and stays there. The index-1950 control similarly moves to 1751. On the previous head, that control stayed at 1950 through both resizes.

That's a resize of container scenario after the requested item is correctly aligned to the top of the viewport and the alignment process finishes. I think it's out of scope for this PR and maybe in general for virtualize component. We don't promise that jump will survive the resize.

The CI failure of QuickGrid_InitialIndex_TallContainer_NearEnd_FillsViewportWithoutUserScroll is a flaky test, I have a 1/21 failure rate reproduction and I am looking into it.

@ilonatommy
ilonatommy merged commit fde48e9 into main Aug 21, 2026
29 checks passed
@ilonatommy
ilonatommy deleted the fix-67933-small-initial-index branch August 21, 2026 13:43
@ilonatommy

Copy link
Copy Markdown
Member Author

/backport to release/11.0-rc1

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/11.0-rc1 (link to workflow run)

@PureWeen

PureWeen commented Aug 21, 2026

Copy link
Copy Markdown
Member

Human + Copilot disclosure: This reply comes from a joint review by Shane Neuville and GitHub Copilot.

We don't promise that jump will survive the resize.

Thanks, that's a useful distinction. I agree Virtualize doesn't promise that an exact anchor will survive an arbitrary container resize. But after rerunning the exact current head, 944cbbd0808c62096fac2136882b926c548e214b, I don't think that's what this repro is showing.

The list initially puts item 1950 at the top for InitialItemIndex=1990, but its scroll height is already only 58,790px instead of the expected 100,000px. On the first 2500px -> 2000px resize, the first visible item moves to 1362 and the scroll height changes to 83,175px. It stays there through three more smaller/larger resize cycles.

The resize is exposing state that was corrupted during initial alignment:

  1. JS measures 25 rendered rows with a 1,250px separation.
  2. A 62-row window commits before that asynchronous result is consumed.
  3. .NET applies the old 1,250px measurement using the current 62-row count.
  4. The estimated row size changes from 50px to about 20.16px, which changes the spacer geometry used by the next redistribution.

So I'm not asking for a new promise that the exact jump target always survives resize. I'm asking that geometry from an older rendered slice not recalibrate a newer slice. That mismatch affects the component's internal size model and future virtualization behavior.

I repeated the same candidate-independent browser assertion in detached worktrees. The frozen ab1e067c implementation was red in 3/3 runs, and a correction that carries a monotonic rendered-window version with alignment and both spacer measurements was green in 3/3 runs for the 1990 case, the 1950 control, and a variable-height control. I then ran the exact 944cbbd0 head and it still fails the same assertion. I also had MAI, Claude Sonnet, and GPT Terra independently challenge whether this needed correction. All three concluded that it did after separating the GitHub head from the uncommitted candidate.

I agree the QuickGrid CI failure should be handled as a separate flake investigation. I'm not relying on that failure for this request.

I see this merged while I was finishing the current-head validation, and the release/11.0-rc1 backport has started. Could we take this result into account for the backport and decide whether to address it there or track it as an immediate follow-up?

wtgodbe pushed a commit that referenced this pull request Aug 21, 2026
…iner or on window resize (#67936) (#68689)

Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com>
wtgodbe added a commit that referenced this pull request Aug 22, 2026
* [SignalR] Reject duplicate SignalR upload stream IDs (#68525) (#68638)

* Reject duplicate SignalR upload stream IDs



* Simplify upload stream ownership cleanup



* Avoid upload stream ownership allocations



* Simplify upload stream registration ownership



* Defer upload stream reader creation





* Dispose cancellation source after binding failure





* Reuse upload stream test helper





---------

Co-authored-by: Javier Calvarro Nelson <jacalvar@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 82e97f5a-a052-4dbe-9cf1-b62f45cf7ee2

* Honor all sign-in confirmation requirements after registration (#68631) (#68655)

Co-authored-by: Brennan <brecon@microsoft.com>

* Preserve BadHttpRequestException status codes (#68632) (#68649)

* Preserve BadHttpRequestException status codes



* Preserve exception handler 404 safeguard



---------

Co-authored-by: Stephen Halter <halter73@gmail.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* SignInManager: return SignInResult.Failed for expired passkey session challenge (#67539) (#68654)

Co-authored-by: Grant Totinov <granttotinov604@gmail.com>

* Don't apply the CSRF verdict to remote authentication callbacks (#68669)

* Don't apply the CSRF verdict to remote authentication callbacks

A remote provider's callback (OIDC response_mode=form_post, WS-Federation)
is a cross-site form POST by protocol design, so the auto-injected CSRF
protection records an invalid IAntiforgeryValidationFeature verdict for it.
The handler then throws while reading its own callback body, before any of
its events can run, so apps have no way to opt out.

Suppress the verdict while a remote handler owns the request, and restore it
if the handler declines so the rest of the pipeline still sees it.

Fixes #68666

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cb987098-3301-465b-9a3d-2e63aabf43bd

* test both antiforgery & csrf

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cb987098-3301-465b-9a3d-2e63aabf43bd

* Use model display names in Blazor input parsing errors (#68667) (#68688)

* Use display attributes in input parsing errors

* Address test coverage feedback from review.

* Apply dedup cleanup from feedback.

Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com>

* [release/11.0-rc1] Extract IsAuthenticated helper method (#68658)

* Extract IsAuthenticated helper method

Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com>

* Reorder using

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com>

* Use SecurityHelper for authentication revalidation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com>

---------

Co-authored-by: Youssef1313 <youssefvictor00@gmail.com>
Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com>
Co-authored-by: Milos Kotlar <kotlarmilos@gmail.com>

* Fix  InitialItemIndex viewport underfill for small items in big container or on window resize (#67936) (#68689)

Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com>

* fix nullable<union> for openapi gen (#68665)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Javier Calvarro Nelson <jacalvar@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Brennan <brecon@microsoft.com>
Co-authored-by: Stephen Halter <halter73@gmail.com>
Co-authored-by: Grant Totinov <granttotinov604@gmail.com>
Co-authored-by: Korolev Dmitry <dmkorolev@microsoft.com>
Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Youssef1313 <youssefvictor00@gmail.com>
Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com>
Co-authored-by: Milos Kotlar <kotlarmilos@gmail.com>
Co-authored-by: William Godbe <wigodbe@microsoft.com>
Copilot-Session: 82e97f5a-a052-4dbe-9cf1-b62f45cf7ee2
Copilot-Session: cb987098-3301-465b-9a3d-2e63aabf43bd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-reviewed area-blazor Includes: Blazor, Razor Components feature-blazor-virtualization This issue is related to the Blazor Virtualize component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Virtualize doesn't render items for small InitialIndex until the first scroll

6 participants