Skip to main content

List Virtualization

Virtualization (windowing) renders only the items that are visible in the viewport, plus a few around it, instead of the whole list. The number of DOM nodes stays constant whatever the size of the data, which fixes slow first render, slow updates and high memory use. Current libraries: TanStack Virtual (a headless hook), react-window and React Virtuoso (ready components). react-virtualized is no longer maintained.

Applies to: TanStack Virtual 3.

Details​

How It Works​

  • The scroll container has a fixed height and overflow: auto.
  • An inner element has the full height of the list, so the scrollbar is real.
  • Only the visible items are rendered, absolutely positioned at their offsets.
  • On scroll the library recalculates which indexes are visible.
  • Overscan is a few extra items above and below, so fast scrolling does not show blank space.
  • Item size is either fixed, or estimated first and then measured (dynamic heights).

When to Use​

  • Thousands of rows, or hundreds of heavy ones.
  • For a few hundred simple items it is usually unnecessary.
  • First check whether pagination or "load more" solves the problem.

Trade-offs​

  • The browser's find-in-page does not see items that are not rendered.
  • Screen readers see only the rendered part: the total needs aria-rowcount or aria-setsize.
  • Dynamic heights can make the scroll position jump.
  • Scroll restoration and "scroll to item" need the library API.
  • React Compiler skips components that call useVirtualizer: the hook returns functions that can't be memoized safely.

Alternatives​

  • Pagination or infinite scroll (often combined with virtualization).
  • CSS content-visibility: auto: the nodes stay in the DOM, but the browser skips layout and paint for the off-screen ones.

Example​

import { useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';

export default function VirtualList({ items }) {
const parentRef = useRef(null);

const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
overscan: 5,
});

return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index]}
</div>
))}
</div>
</div>
);
}

See Also​