List Virtualization

Difficulty: Advanced

List virtualization (also called windowing) is a technique for efficiently rendering large lists by only mounting the DOM nodes that are currently visible in the viewport. Instead of rendering 10,000 list items (creating 10,000 DOM nodes), virtualization renders only the ~20 items visible on screen plus a small buffer (overscan). As the user scrolls, items entering the viewport are mounted and items leaving are unmounted.

Without virtualization, a list of 10,000 items creates 10,000 DOM nodes. Each node takes memory, and the browser must lay out and paint all of them. Initial render can take several seconds, scrolling becomes janky, and memory usage spikes. Users on mobile devices with limited RAM may see the browser tab crash.

The react-window library (by Brian Vaughn, a React core team member) is the standard solution. It provides FixedSizeList for items with uniform height and VariableSizeList for items with different heights. You provide the total item count, item size, and a render function. react-window calculates which items are visible based on scroll position and only renders those.

The overscan property controls how many items are rendered outside the visible area. A small overscan (1-2 items) gives faster scroll performance but may show brief blank areas during fast scrolling. A larger overscan (5-10 items) provides smoother scrolling at the cost of rendering more items. The default of 1 works well for most cases.

Virtualization is most beneficial when you have hundreds or thousands of items. For lists under 100 items, the overhead of virtualization (measuring scroll position, calculating visible range) may exceed the cost of just rendering all items. Use it when you have large datasets and can confirm (via profiling) that rendering is the bottleneck.

Code examples

Basic FixedSizeList with react-window

import { FixedSizeList } from 'react-window';

const items = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: `Item ${i + 1}`,
}));

function Row({ index, style }) {
  const item = items[index];
  return (
    <div style={style} className="flex items-center px-4 border-b">
      <span className="font-medium">{item.name}</span>
    </div>
  );
}

function VirtualList() {
  return (
    <FixedSizeList
      height={400}      // viewport height
      width="100%"
      itemCount={items.length}
      itemSize={50}      // each row is 50px
      overscanCount={5}
    >
      {Row}
    </FixedSizeList>
  );
}

react-window renders only the visible items plus 5 overscan items above and below. The total DOM node count stays around 20 regardless of list length.

VariableSizeList for different item heights

import { VariableSizeList } from 'react-window';

const messages = Array.from({ length: 5000 }, (_, i) => ({
  id: i,
  text: i % 3 === 0
    ? 'This is a longer message that takes up more vertical space in the list.'
    : 'Short message.',
}));

// Height estimation function
const getItemSize = (index) => messages[index].text.length > 30 ? 80 : 40;

function MessageRow({ index, style }) {
  return (
    <div style={style} className="px-4 py-2 border-b">
      <p>{messages[index].text}</p>
    </div>
  );
}

function MessageList() {
  return (
    <VariableSizeList
      height={500}
      width="100%"
      itemCount={messages.length}
      itemSize={getItemSize}
      estimatedItemSize={50}
    >
      {MessageRow}
    </VariableSizeList>
  );
}

VariableSizeList calls getItemSize for each index to determine the row height. estimatedItemSize helps with initial scroll bar sizing before all heights are measured.

Performance comparison: virtual vs non-virtual

// Non-virtualized: renders ALL 10,000 items
function SlowList({ items }) {
  console.time('SlowList render');
  const result = (
    <div style={{ height: 400, overflow: 'auto' }}>
      {items.map(item => (
        <div key={item.id} style={{ height: 50, borderBottom: '1px solid #eee' }}>
          {item.name}
        </div>
      ))}
    </div>
  );
  console.timeEnd('SlowList render');
  return result;
}
// SlowList render: ~200-500ms, 10,000 DOM nodes

// Virtualized: renders ~20 items
// FastList render: ~5ms, ~20 DOM nodes
// Scrolling is smooth because only ~20 items are in the DOM at any time

The non-virtualized list creates 10,000 DOM nodes taking hundreds of milliseconds. The virtualized list creates ~20 nodes in under 5ms. The difference grows exponentially with list size.

Key points

Concepts covered

virtualization, windowing, react-window, overscan, variable-size lists, scroll performance