Windowing Long Lists

A list of 20,000 rows costs 20,000 DOM nodes, and the browser pays for every one in layout, style recalculation and memory long after React 7,897 has finished. Windowing — virtualization — renders only the rows inside the scrollport plus a small overscan, translating them into position as you scroll. react-window 17,208 (https://github.com/bvaughn/react-window 17,208 ) 2.3.1 (MIT, npm 2,036 i react-window) is the smallest option; its version 2 API takes a rowComponent instead of a render-prop child.

20,000 rows, plain and windowedHTML
<b>plain</b><div id="plain"></div><b>windowed</b><div id="win"></div><p id="out"></p>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0",
  "react/jsx-runtime": "https://esm.sh/react@19.3.0/jsx-runtime",
  "react-dom": "https://esm.sh/react-dom@19.3.0",
  "react-dom/client": "https://esm.sh/react-dom@19.3.0/client",
  "react-window": "https://esm.sh/react-window@2.3.1?external=react" } }</script>
<script type="text/babel" data-type="module">
  import React from 'react';
  import { createRoot } from 'react-dom/client'; import { flushSync } from 'react-dom';
  import { List } from 'react-window';
  const N = 20000, box = { height: 40, font: '13px system-ui' };
  const Row = ({ index, style }) => <div style={style}>customer {index}</div>;
  const time = (id, el) => { const root = createRoot(document.getElementById(id));
    const t = performance.now(); flushSync(() => root.render(el));
    return (performance.now() - t).toFixed(1); };
  const b = time('win', <List rowComponent={Row} rowCount={N} rowHeight={18}
    rowProps={{}} style={box} />);
  const a = time('plain', <div style={{ ...box, overflow: 'auto' }}>
    {Array.from({ length: N }, (_, i) => <div key={i}>customer {i}</div>)}</div>);
  const n = (id) => document.querySelectorAll('#' + id + ' *').length;
  out.innerHTML = `plain ${a} ms / ${n('plain')} nodes, windowed ${b} ms / ${n('win')} nodes`;
</script>
Browser output of Listing 2.102
Browser output of 102

The windowed mount is about four times faster, but the node counts differ by three orders of magnitude, and that is what keeps mattering: every later scroll, resize and style change walks the smaller tree. Note the measurement order — mounting the windowed list after the 20,000-node one forces a full-document layout and inflates its time several-fold, a benchmark artifact rather than the library's doing.

Windowing has real costs: Ctrl+F finds nothing that is not rendered, and variable row heights need a measurement pass (useDynamicRowHeight). Accessibility is handled for you — spread the ariaAttributes prop List hands each row onto your row element. TanStack Virtual 79,069 is the headless alternative, react-virtuoso adds dynamic heights and infinite scroll, and react-virtualized is in maintenance mode.