Skip to content

Fix AnchorMode_End_PrependAtTop_ViewportStaysStable - #68064

Open
ilonatommy wants to merge 5 commits into
dotnet:mainfrom
ilonatommy:fix-virtualize-prepend-at-top-server-race
Open

Fix AnchorMode_End_PrependAtTop_ViewportStaysStable#68064
ilonatommy wants to merge 5 commits into
dotnet:mainfrom
ilonatommy:fix-virtualize-prepend-at-top-server-race

Conversation

@ilonatommy

@ilonatommy ilonatommy commented Jul 28, 2026

Copy link
Copy Markdown
Member

Summary

Fixes a flaky failure in ServerVirtualizationTest.AnchorMode_End_PrependAtTop_ViewportStaysStable(useItemsProvider: true). The test intermittently failed with the viewport jumping to the prepended items instead of staying stable:

End mode at top: viewport should stay stable after prepend (index before: 0, after: -10, relTop before: 1, after: 1, scrollTop: 0, tolerance: 5)

Root cause

This is a Blazor Server timing race, not a logic error (the anchor mechanism is correct).

When items are prepended while the viewport is at the very top (scrollTop == 0), Virtualize preserves the pre-shift anchor snapshot and relies on RestoreAnchorAsync to reposition the scroll so the previously-visible row stays at its place. On Server, RestoreAnchorAsync is a SignalR round-trip. Because the grown spacerBefore is already visible at scrollTop == 0, its IntersectionObserver fires and dispatches OnSpacerBeforeVisible to the server, loading the prepended rows (-10..-1) into the viewport before the restore round-trip completes. An async ItemsProvider with delay widens the window, making the race more probable to happen. WebAssembly/synchronous paths don't race because restore is effectively synchronous.

Fix

In the snapshot-preserve branch of updateAnchorSnapshot, set suppressSpacerCallbacks = true only when the pin originated at the very top (existing.scrollTop < 1) and the anchor mode is End. This blocks the racing spacerBefore callback from dispatching a load to the server for the one render cycle until the anchor restore runs. The suppression is cleared on the next user scroll (handleScroll -> reobserveSpacers), so the prepended rows remain fully reachable when the user scrolls up.

The existing.scrollTop < 1 gate is required: an unconditional suppress would re-suppress the spacer every time the user scrolled back to the top, permanently preventing the above-viewport rows from loading and breaking the "prepended items become reachable" assertion.

…apshot and avoid ignoring spacer being visible.
@ilonatommy ilonatommy added this to the 11.0-rc1 milestone Jul 28, 2026
@ilonatommy ilonatommy self-assigned this Jul 28, 2026
@ilonatommy
ilonatommy requested a review from a team as a code owner July 28, 2026 13:27
Copilot AI review requested due to automatic review settings July 28, 2026 13:27
@ilonatommy ilonatommy added area-blazor Includes: Blazor, Razor Components feature-blazor-virtualization This issue is related to the Blazor Virtualize component labels Jul 28, 2026

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

Fixes a timing race in Blazor Server virtualization that could cause the viewport to jump when items are prepended at the very top in End anchor mode, leading to flakiness in ServerVirtualizationTest.AnchorMode_End_PrependAtTop_ViewportStaysStable(useItemsProvider: true).

Changes:

  • When updateAnchorSnapshot() preserves the pre-shift snapshot, it now suppresses spacer IntersectionObserver callbacks only if the preserved snapshot originated at the very top (existing.scrollTop < 1).
  • This prevents spacerBefore visibility callbacks from racing ahead of the anchor-restore round-trip specifically in the “pinned at top” scenario, while avoiding suppression in cases that would break reachability of prepended rows in other modes.

ilonatommy and others added 4 commits July 28, 2026 17:29
The scrollTop<1 suppression added for the End-mode prepend-at-top race
also fired for None mode (!anchorModeIs.beginning). In None mode there is
no anchor restore to race, so suppressing the before-spacer callback at the
top leaves the topmost item unloaded with no user scroll to clear it -
regressing default Virtualize scroll-to-top (e.g. a 50px before spacer that
never converges to 0). Restrict the suppression to End mode, which is the
only mode that pins the top row while prepended items land.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fe1ee7a6-0928-46b2-bbb1-0d9df6adee51
Mirror AnchorMode_Start_LargePrependAtTop_StillShowsNewItems for None and
End modes, prepending 100 items at the top and asserting the viewport stays
stable (the same item keeps its position). None additionally verifies the
prepended items are reachable after scrolling to the top. Covers the
large-prepend + variable-height + delayed-provider combination that was only
exercised for Start mode.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fe1ee7a6-0928-46b2-bbb1-0d9df6adee51
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 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.

2 participants