renderpx
Theme: auto

Virtualized Lists

Render only the visible rows (plus a small overscan) so long lists stay performant without thousands of DOM nodes.

The problem I keep seeing

Feeds, tables, and dropdowns sometimes need to display thousands of items. Rendering every item creates thousands of DOM nodes, which slows layout and paint and makes scrolling janky. You need to render only what’s in (or near) the viewport and reuse nodes as the user scrolls.

Naive approach

Map over the full array and render one node per item. Simple and fine for a few hundred items; for 10k+ it becomes a bottleneck.

tsx
Loading...

First improvement

“Window” the list: from the scroll position and container height, compute the range of indices that are visible (plus a small overscan). Render only those items and use a spacer (height = startIndex * itemHeight) and transform: translateY so the scrollable height and scroll position stay correct.

tsx
Loading...

Remaining issues

  • Variable height: If items have different heights, you need to measure (or estimate) and compute offsets; a library handles this with a size cache.
  • Scroll restoration: When navigating back to the list, restoring scroll position requires storing and reapplying the scroll offset (or the first visible index).
  • Accessibility: Screen readers expect to know list length and may virtualize differently; use aria-setsize / aria-posinset and consider “load more” or a non-virtualized fallback for assistive tech if needed.

Production pattern

Use @tanstack/react-virtual or react-window. They handle visible range, overscan, total size, and (with the right options) variable height. Prefer a stable estimateSize for fixed rows; use dynamic measurement when heights vary. Keep list semantics (role="list", role="listitem") and add aria-setsize / aria-posinset if you care about screen reader list navigation.

tsx
Loading...

Variable height

For variable-height items, use estimateSize and let the library measure (e.g. measureElement in TanStack Virtual). The snippet above shows the idea; see library docs for the exact API.

When I use this

  • Long lists (500+ items): Feeds, tables, select dropdowns with many options. Virtualization keeps DOM small and scroll smooth.
  • Skip when: List has < 100–200 items; the overhead of virtualization isn’t worth it and full render is fine.

Gotchas

  • Stable keys: Use a unique, stable id per item so when the window shifts, React doesn’t remount unnecessarily.
  • Overscan: Render a few extra items above and below the viewport so fast scrolling doesn’t show blank gaps; 5–10 is usually enough.
  • Infinite scroll + virtual: You can combine with useInfiniteQuery: virtualize the concatenated pages and grow the list as the user scrolls near the end.

Infinite Scroll → · All patterns