QuestionsWeb Performance

Virtualizing long lists (windowing)

List VirtualizationHardWeb Performance

Rendering 10,000 rows freezes the page. Explain windowing, what it costs, and the tricky parts.

What it tests

Understanding why huge DOMs are slow and how to render only what's visible while preserving scroll.

Approach & answer

Rendering 10,000 rows creates 10,000+ DOM nodes: that's slow to build, expensive in memory, and every style/layout pass has to consider all of them — so initial render, scrolling, and updates all jank. Windowing (virtualization) renders only the rows currently in (or near) the viewport — maybe 20 — plus a small overscan buffer, and recycles them as you scroll. The core mechanics: (1) A tall spacer establishes the full scrollable height so the scrollbar behaves as if all rows exist (height = rowCount × rowHeight for fixed rows). (2) On scroll, compute which index range is visible from scrollTop and rowHeight, and render only that slice, absolutely positioned (or offset with translateY/padding) at the right place. (3) As the user scrolls, the visible slice shifts and you render different rows into roughly the same handful of nodes. The costs and tricky parts: FIXED-height rows are easy; VARIABLE heights require either measuring rows and caching their offsets (a prefix-sum you update as measurements come in) or estimating then correcting, which can cause scroll jumps if estimates are off. Accessibility and find-in-page break because off-screen rows aren't in the DOM — mitigate with appropriate ARIA (row/rowcount) and by not virtualizing when the list is short. Anchoring/jumpiness on prepend needs scroll-position compensation. Sticky headers, keyboard focus on a recycled node, and horizontal + vertical grids add complexity. In practice you reach for a battle-tested library (react-window / TanStack Virtual) rather than hand-rolling, but you must understand the model to debug it. An adjacent modern option for pure show/hide cost is CSS `content-visibility: auto` with `contain-intrinsic-size`, which lets the browser skip rendering off-screen subtrees while keeping them in the DOM — cheaper to adopt, though it doesn't cut node count or memory the way true windowing does.

Use this technique when

Rendering very long lists/tables/feeds without freezing; choosing windowing vs. content-visibility.

Complexity

Renders O(visible) nodes instead of O(total); scroll math O(1) for fixed-height rows

Code

function VirtualList({ items, rowHeight = 40, height = 400, overscan = 5 }) {
  const [scrollTop, setScrollTop] = React.useState(0);
  const total = items.length * rowHeight;

  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
  const visible = Math.ceil(height / rowHeight) + 2 * overscan;
  const end = Math.min(items.length, start + visible);
  const slice = items.slice(start, end);

  return (
    <div style={{ height, overflow: 'auto' }}
         onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}>
      {/* spacer gives the real scroll height */}
      <div style={{ height: total, position: 'relative' }}>
        {slice.map((item, i) => (
          <div key={start + i}
               style={{ position: 'absolute', top: (start + i) * rowHeight,
                        height: rowHeight, width: '100%' }}>
            {item.label}
          </div>
        ))}
      </div>
    </div>
  );
}

References