ESC

Type to search the knowledge base.

List Virtualization

Render only visible rows for large lists: windowing mental model, libraries, keys, and accessibility tradeoffs.

advanced4 min read
  • react
  • list-virtualization

Mounting 50,000 DOM nodes freezes the main thread. List virtualization (windowing) renders only the rows near the viewport plus a small overscan buffer, recycling row components as the user scrolls.

Docs / libs: TanStack Virtual, react-window.

Mental model

  1. Know total count and row height (fixed or measured).
  2. From scroll offset, compute first/last visible index.
  3. Render only those items, positioned absolutely (or via transforms) inside a tall spacer that preserves scroll height.
// Conceptual — prefer a battle-tested library in production
function VirtualList({ items, rowHeight, height }) {
  const [scrollTop, setScrollTop] = useState(0);
  const start = Math.floor(scrollTop / rowHeight);
  const visibleCount = Math.ceil(height / rowHeight) + 2;
  const slice = items.slice(start, start + visibleCount);

  return (
    <div
      style={{ height, overflow: 'auto' }}
      onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
    >
      <div style={{ height: items.length * rowHeight, position: 'relative' }}>
        {slice.map((item, i) => {
          const index = start + i;
          return (
            <div
              key={item.id}
              style={{
                position: 'absolute',
                top: index * rowHeight,
                height: rowHeight,
                left: 0,
                right: 0,
              }}
            >
              {item.label}
            </div>
          );
        })}
      </div>
    </div>
  );
}

Keys and state

Use stable item ids, not indices — recycling makes index keys catastrophic (preserving state). Local state in a row may reset as the component is reused for another item unless you key by id carefully and design for reuse.

Variable height and grids

Variable heights need measurement (libraries handle this). Grids virtualize 2D windows. Nested scrollers need clear ownership of scroll containers.

Accessibility

Virtualization can break “find in page,” screen reader virtual buffers, and tabindex order. Provide:

  • sensible aria-rowcount / set size where applicable
  • keyboard scroll and focus management
  • an alternative for export/search when the full list is not in the DOM

When not to virtualize

Hundreds of simple rows often fine. Virtualize when profiling shows mount/layout cost. Also consider pagination or infinite query windows from the server.

Interview out-loud

“Virtualization renders only visible rows plus overscan inside a spacer that preserves scroll height. I use stable ids, a known or measured row height, and a library for edge cases. I call out a11y and find-in-page tradeoffs, and I virtualize only after measuring.”

Further reading

Edge cases worth rehearsing

Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.

Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.

When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.

Edge cases worth rehearsing

Interviewers and production incidents cluster around the same edges: first render versus update, empty and loading states, Strict Mode double setup, concurrent interruptions, and what happens when identity (key, route params, user id) changes mid-edit. Walk one concrete user journey end-to-end — open, edit, navigate away, come back — and say which state survives.

Prefer fixing data flow and ownership before reaching for memoization or micro-optimizations. Prefer event handlers over effects when a user action is the trigger. Prefer deriving values during render over mirroring props into state. Prefer stable list keys from business ids. Measure with the profiler when performance is the claim.

When you cite an API, mention one failure mode: abort on unmount, serializable props across server/client boundaries, focus restoration for dialogs, or cache invalidation after a mutation. Specific beats generic every time.

Related guides