feat: add RecycleList virtualized list component#207
feat: add RecycleList virtualized list component#207haywoodfu wants to merge 17 commits intoDioxusLabs:mainfrom
Conversation
Add a new RecycleList component that virtualizes large lists by rendering only the visible slice plus a configurable buffer. Supports dynamic row heights and both container-scroll and window-scroll modes. Primitive (dioxus-primitives): - RecycleList with configurable items, buffer, render_item callback - Spread attributes support via attributes: Vec<Attribute> - Scroll tracking via document::eval() JS bridge (no extra deps) - Automatic container-scroll vs window-scroll detection Preview: - Styled wrapper with shadcn theme - Demo variant with 2000 dynamic-height rows - Component metadata (component.json, docs.md) Tests: - Playwright E2E test for virtualization behavior Closes DioxusLabs#203
|
Preview available at https://dioxuslabs.github.io/components/pr-preview/pr-207/ |
ealmloff
left a comment
There was a problem hiding this comment.
This is a great start, thanks for working on this! When moving the scrollbar, the cursor can move faster than the scrollable content appears:
Screen.Recording.2026-03-02.at.8.23.26.AM.mov
There are also some accessibility considerations for virtual lists. Since not all elements accessible on the page are visible in the dom, they will not show up in screen readers. We need to set special aria attributes to make sure the item count in the list is calculated based on the virtual instead of physical size. Some resourecs:
- React aria handles this well: https://react-aria.adobe.com/Virtualizer
- This is one of the attributes we need for non-interactive lists: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-setsize
…ility - Replace borrowed-slice API (items: &[T]) with index-based API (count + Callback<usize, Element>) - Use #[derive(Props, Clone, PartialEq)] and #[component] for standard RSX struct syntax - Add ARIA attributes (role=list, role=listitem, aria-setsize, aria-posinset) - Use use_unique_id() instead of hardcoded DOM id for multi-instance support - Update preview wrapper, demo variant, and docs to match new API Addresses review feedback from PR DioxusLabs#207.
- Replace spacer-based layout with virtual canvas + translateY content layer - Coalesce scroll events via requestAnimationFrame to reduce JS-to-Rust sync frequency - Buffer height measurements during active scrolling, flush on idle (180ms debounce) - Skip re-measurement for already-measured items via measured_flags - Add CSS containment (contain: layout paint) on scroll container - Flatten nested signal with_mut calls to avoid potential lock contention
…-only mode - Increase toBeVisible timeout to 30s for slower WASM init in Firefox CI - Remove window.scrollTo fallback since scroll listener is now container-only
- Add back window scroll listener in JS bridge for compatibility with preview app's duplicate component rendering (SSR hydration) - Cap viewport at window.innerHeight to prevent over-rendering before CSS loads - Make scroll test scroll all containers and check for high-index items, robust against duplicate container instances - Increase toBeVisible timeout to 30s for slower WASM init in Firefox CI
|
Scrolling looks much better, thanks! It looks like there are some issues with the view jumping after scrolling (I assume as the dead nodes unload?) jumping.mov |
Defer height updates until scrolling settles, preserve the visible anchor when measurements flush, and adapt estimates for unmeasured rows to reduce jumpiness in virtualized regions.
Preserve the first visible DOM item when flushing measured heights, keep adaptive estimates for unmeasured rows, and refactor height cache updates so virtualized content stays stable when entering new regions.
- Use adaptive height estimates in prefix sum calculation (use_memo) so unmeasured items get calibrated heights as soon as the first screen renders, eliminating the jump on first scroll to unknown regions - Match apply_pending_measurements prefix sums to the adaptive layout via build_prefix_sums_adaptive, ensuring anchor compensation targets the actual rendered positions - Replace .read() with .peek() in onmounted to avoid unnecessary signal subscriptions - Remove dead build_prefix_sums after consolidating into the adaptive variant - Clone container_id once per flush block instead of three times
Thank you for reviewing! The scroll jump was caused by a mismatch between the virtual height model and the actual rendered DOM when entering an unmeasured region for the first time. At that point, the list was still using estimated heights for unseen rows. Once those rows mounted and their real heights were measured, the cached height map and prefix sums changed, which could shift the virtual offset and make the viewport jump. I fixed this in two parts:
Before flushing pending measurements, the list captures the first actually visible item in the viewport and its pixel offset relative to the container. After the height cache is updated, it restores the viewport using that same DOM anchor instead of relying only on the theoretical virtual offset. This removed the visible jump when scrolling into previously unmeasured regions, while keeping the existing virtualization behavior intact. |
The examples! macro unconditionally includes style.css for syntax highlighting. An empty file causes syntect to fail, so add a comment- only placeholder.
Add a new RecycleList component that virtualizes large lists by rendering only the visible slice plus a configurable buffer. Supports dynamic row heights and both container-scroll and window-scroll modes.
Primitive (dioxus-primitives):
Preview:
Tests:
Closes #203