Skip to content

feat: add RecycleList virtualized list component#207

Open
haywoodfu wants to merge 17 commits intoDioxusLabs:mainfrom
haywoodfu:feat/recycle-list
Open

feat: add RecycleList virtualized list component#207
haywoodfu wants to merge 17 commits intoDioxusLabs:mainfrom
haywoodfu:feat/recycle-list

Conversation

@haywoodfu
Copy link
Contributor

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
  • 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 #203

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
@github-actions
Copy link

Copy link
Member

@ealmloff ealmloff left a comment

Choose a reason for hiding this comment

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

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:

…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
@haywoodfu haywoodfu requested a review from ealmloff March 9, 2026 11:53
haywoodfu and others added 5 commits March 9, 2026 11:56
…-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
@ealmloff
Copy link
Member

ealmloff commented Mar 9, 2026

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
@haywoodfu
Copy link
Contributor Author

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

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:

  • I made the height model consistent between rendering and compensation. Unmeasured rows now use the same adaptive estimate both when computing the visible range and when recalculating offsets after measurements are flushed.

  • I switched the scroll restoration logic to anchor against the real DOM.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add RecycleList (virtualized list)

2 participants