Repository navigation
Send the item set ahead of the render that shows it - #6
Merged
Merged
Conversation
InvalidateItemSize used to evict the key, so the rows below fell back to the estimate until something measured the row again. Once the item set travels ahead of the render that changes the row, that would move every row below to the estimate around a row that had not changed yet. A rendered row is now measured as it is; virtual-core's ResizeObserver takes its new size when its content changes. A row that is not rendered still falls back to the estimate.
setItems went out from OnAfterRenderAsync, one round trip after the render that draws the rows a new item set moves - hidden, because their placements are stale until JS holds the set. A WebView paints in between: on a Samsung A53 every row below an expanding group was painted hidden for up to 220 ms. OnParametersSet now sends the set before the render, so the observer that sees the render's rows places and shows them before the frame is painted. OnAfterRenderAsync awaits the send, and still sends a set that changed while the instance was being created.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6 +/- ##
==========================================
+ Coverage 77.91% 78.67% +0.76%
==========================================
Files 7 7
Lines 335 347 +12
Branches 51 52 +1
==========================================
+ Hits 261 273 +12
Misses 64 64
Partials 10 10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
Item updates can use outdated options, while measurement invalidation and deferred resize handling can retain stale row sizes, causing incorrect append-follow behavior, scroll offsets, and row overlap.
3 open issues
What changed in this PR
This release updates Blazor-to-JavaScript synchronization to prevent hidden-row flashes during item-set changes and preserve rendered row sizes during invalidation.
Changes:
- Sends changed item sets before rendering, then awaits outstanding sends after rendering.
- Measures rendered rows during size invalidation while retaining the estimate fallback for unrendered rows.
- Adds regression coverage, tightens painted-frame assertions, and updates the package to 3.19.8.4.
| File | Description |
|---|---|
tests/Blazor.TanStack.VirtualCore.Tests/AdaptiveVirtualizeTests.cs |
Tests pre-render item dispatch and updates disposal call-order expectations. |
tests/Blazor.TanStack.VirtualCore.E2E/SlowInteropTests.cs |
Requires zero painted frames with hidden rows. |
tests/Blazor.TanStack.VirtualCore.E2E/InteropModuleTests.cs |
Tests rendered-row size preservation through invalidation and growth. |
src/Blazor.TanStack.VirtualCore/wwwroot/adaptive-virtualize-interop.js |
Adds rendered-row handling during invalidation and clarifies timing comments. |
src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj |
Updates the package version and release notes. |
src/Blazor.TanStack.VirtualCore/AdaptiveVirtualize.razor.cs |
Dispatches item sets before rendering and awaits queued sends afterward. |
Review effort: Balanced
setItems reacts to the item count and edge keys through options such as FollowOnAppend and AnchorTo. Sent ahead of the render while setOptions still went out after it, a change of both in one render applied the append behaviour of the options being replaced. Both now go out from OnParametersSet, options first, as they did when both went out after the render.
virtual-core answers a measurement without a ResizeObserver entry from its size cache. invalidateItemSize measured through it and so kept the size the row had before; it now reads the row itself, and still drops the size under useCachedMeasurements, where nothing reads rows. A row of the item set that setItems just replaced dropped a resize it got before its render landed, and the measure pass after the render read the old size back; that size is now kept under the row's own key. Release notes updated for both.
A size read off a row of this instance's item set now goes through virtual-core's resizeItem, which corrects the scroll offset when a row above the viewport changes size; written straight into the cache, the content jumped by the difference. A size kept for a row still drawn from the replaced set is written directly, and resets pendingMin so the recompute reaches its index. virtual-core drops a ResizeObserver entry whose index is past the item count, so setItems now notes the rows still drawn from the set it replaced, and the measure pass after the render reads their sizes off the rows.
measureRenderedRows tested keys for truthiness, so a row keyed "" was never recorded as measured there and skipped the re-read of rows from a replaced item set. The ResizeObserver does record "" as measured, so such a row that resized past the new count before its render landed kept its old size.
The right-to-left scroll write and the stale getOffsetForIndex are upstream bugs with open fixes (TanStack/virtual PRs 1291 and 1363); the table names them and the tests that fail once a bump makes each stopgap wrong. InvalidateItemSize now measures a rendered item, and the internals note lists pendingMin and the proposed public API (TanStack/virtual discussion 1362).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Release 3.19.8.4.
Send the options and the item set ahead of the render that shows them
After an Items change the rows whose placement went stale render hidden until the JS side holds the new set (3.19.8.3). setItems went out from
OnAfterRenderAsync, one round trip after that render, and a WebView paints in between: on a Samsung A53 expanding a group in an exercise picker painted every row below it hidden in 3 of 5 expands, for 48-224 ms - a dark band flashing under the group.OnParametersSetnow sends setOptions and setItems, in that order, before the render. By the time the render's rows reach the DOM the JS side holds their keys, and the MutationObserver that sees them places and shows them before the frame is painted. Options go first because setItems reacts to the count and edge keys through FollowOnAppend and AnchorTo.OnAfterRenderAsyncawaits the sends, and still sends what changed while the instance was being created.Measure rows as they are rendered
InvalidateItemSizereads a rendered row's size off the row instead of dropping it, so a set that arrives before the row's content changes no longer moves the rows below to the estimate; virtual-core's ResizeObserver takes the new size when the content changes. A row that is not rendered, or any row underUseCachedMeasurements, still falls back to the estimate.resizeItem, which keeps its scroll correction for rows above the viewport.pendingMinreset so the recompute reaches it). virtual-core drops a ResizeObserver entry whose index is past the new count before it reaches us, so setItems notes the rows still drawn from the replaced set and the measure pass after the render reads their sizes off the rows.Measured
Release build with this package from a local feed, the picker's first group expanded and every painted frame sampled, 10 expands per device:
The three layouts are: collapsed, the grown group with the row below already under it, the inserted rows in place. Some Android samples showed the grown group over the next row; a ResizeObserver on the group showed each of them read a layout no frame had painted yet (the sampler task ran after a render batch and before the next frame), and none was ever painted.
Upstream
scrollToFnhere): already reported, ScrollToIndex is not working with RTL. TanStack/virtual#1065, fix in fix(virtual-core): restore programmatic scrolling in RTL elements TanStack/virtual#1291 - confirmed there with tests.getOffsetForIndexreading stale measurements: getOffsetForIndex returns stale offsets between a size change and the next render TanStack/virtual#1361, fix in fix(virtual-core): compute getOffsetForIndex from the current measurements TanStack/virtual#1363.The README's stopgap table names each with the test that fails once a bump makes the stopgap wrong.
Tests
Each new test failed without its change:
InteropModuleTests: an invalidated row on screen keeps its rendered size; one that already changed takes its current size; under cached measurements it falls back to the estimate; a row above the viewport that already grew keeps the content in place; a row of the replaced set keeps its new size once rendered again, also when it resized past a shrinking set's count, also with an empty key, and also after a later row resized first in the same ResizeObserver batch.SlowInteropTestsnow allows no painted frame with hidden rows (it allowed one). The in-browser demo answers too fast to fail it without the change; the unit test covers that.Unit 44/44, E2E 50/50 locally; CI green.