Repository navigation
Keep rows across Items changes and place them from JS - #5
Merged
Merged
Conversation
When the item set changes, a placement can carry a key that no longer matches the item at its index until JS sends fresh placements. Those rows used to be skipped, so every row component was disposed on each item-set change and built again one round trip later - even rows that are in both sets - with their media, images and JS state. They are now rendered under their own key and hidden until a matching placement arrives. In an exercise picker on a Samsung A53 WebView this cut the render batches of a query change by 23% in bytes and the components created and disposed by 25%.
Placements reached the DOM only through a .NET render, one interop round trip after the virtualizer computed them - ~250 ms in a Blazor Hybrid app on a Samsung A53. Expanding a group showed both costs: the grown row painted over the rows below it for that long, and once the inserted rows shifted every index the rows below stayed hidden for another ~300 ms. Rows now carry data-key, and the JS side moves every row whose key matches the key it holds for that index as soon as it knows the new layout: inside the ResizeObserver callback, before the frame is painted, and when setItems arrives, which also shows the rows .NET hid. A MutationObserver puts back a position an older .NET render writes. On the A53 the expand now paints no overlap at all, and the rows below are hidden for 0-180 ms instead of ~300. Two measurement fixes go with it. A size the ResizeObserver measured now counts as measured, so the setItems after InvalidateItemSize keeps it instead of falling back to the estimate. And a row rendered from an item set JS has not received yet no longer has its size recorded under the key its index still belongs to - the likely way an exercise row once ended up with a section header's height.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5 +/- ##
==========================================
+ Coverage 77.77% 77.91% +0.13%
==========================================
Files 7 7
Lines 333 335 +2
Branches 51 51
==========================================
+ Hits 259 261 +2
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
Rendered-row measurement bypasses key validation, allowing stale calls to persist incorrect row sizes, while deferred iOS scroll corrections can misplace rows.
2 open issues
What changed in this PR
This PR prepares release 3.19.8.3, reducing row component recreation and layout delays in the Blazor virtualizer, especially over slow WebView interop.
Changes:
- Keep rows with stale placements rendered under their item keys but hidden.
- Position matching rows directly from JavaScript and improve resize-measurement handling.
- Add unit and browser regression tests for component retention, measurement correctness, and delayed interop.
| File | Description |
|---|---|
| tests/Blazor.TanStack.VirtualCore.Tests/AdaptiveVirtualizeTests.cs | Tests hidden stale rows, item keys, and component retention. |
| tests/Blazor.TanStack.VirtualCore.E2E/SlowInteropTests.cs | Adds delayed-interop tests for overlap and visibility. |
| tests/Blazor.TanStack.VirtualCore.E2E/InteropModuleTests.cs | Tests measurement preservation and key isolation. |
| src/Blazor.TanStack.VirtualCore/wwwroot/adaptive-virtualize-interop.js | Synchronizes DOM placements and guards resize measurements. |
| src/Blazor.TanStack.VirtualCore/Blazor.TanStack.VirtualCore.csproj | Updates the version and release notes. |
| src/Blazor.TanStack.VirtualCore/AdaptiveVirtualize.razor.cs | Renders mismatched placements as hidden keyed rows. |
| src/Blazor.TanStack.VirtualCore/AdaptiveVirtualize.razor | Adds item keys to row DOM attributes. |
Review effort: Balanced
| @@ -143,6 +173,25 @@ class AdaptiveVirtualizeInstance { | |||
| measureElement: (element, entry, instance) => { | |||
| if (entry) { | |||
| continue; | ||
| } | ||
|
|
||
| const start = toCssNumber(measurement.start); |
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.3.
Keep rows whose placement went stale rendered, hidden
On an Items change a placement can carry a key that no longer matches the item at its index until JS sends fresh placements. Those rows were skipped, so every row component was disposed and built again one round trip later, even rows in both sets. They are now rendered under their own key with
visibility:hiddenuntil a matching placement arrives.Measured in an exercise picker on a Samsung A53 WebView: per query change, 23% fewer render-batch bytes and 25% fewer components created and disposed.
Place rows from JS without waiting for .NET
Placements reached the DOM only through a .NET render, one interop round trip after the virtualizer computed them (~250 ms on the A53). Expanding a group showed both costs: the grown row painted over the rows below it for that long, then the rows below stayed hidden for ~300 ms after the inserted rows shifted every index.
data-key.syncRenderedRowsmoves every row whose key matches the key JS holds for its index: from the ResizeObserver callback (before the frame is painted), and whensetItemsarrives, which also shows rows .NET hid.setItemsafterInvalidateItemSizekeeps it instead of falling back to the estimate.On the A53 (expand traced 5 times, sampled after paint): 0 frames with overlapping rows, rows below hidden for 0-180 ms.
Tests
SlowInteropTests(E2E): holdsOnVirtualItemsChangedback 600 ms and samples every painted frame - a row growing never overlaps the next one, and an expand shows the rows below within a frame. Both fail withoutsyncRenderedRows.InteropModuleTests: a ResizeObserver size survives thesetItemsafterInvalidateItemSize; a row from a newer item set is measured only under its own key. Each fails without its fix.data-key; a row whose key stays keeps its component.Unit 42/42, E2E 42/42 locally.