Virtual Lists — windowing: render only what you see

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ Contrast with frontend/react: effects & lists — normal .map() renders everything; virtualization renders only what's visible.

the windowing idea: O(viewport), not O(data)

A plain items.map() mounts one DOM node per item — 1,000 rows → 1,000 nodes, and the browser re-layouts all of them on every scroll. Virtualization renders only the slice inside the scroll viewport. The scrollbar is faked with a full-height spacer (height = total × itemHeight); the visible rows are absolutely positioned inside it. As you scroll, onScroll → setScrollTop recomputes the slice and React mounts/unmounts just the rows entering or leaving the window.

variableformulaexample (scrollTop = 1200px)
startIndex floor(scrollTop / itemHeight) floor(1200 / 40) = 30
visibleCount ceil(viewportHeight / itemHeight) + overscan ceil(400 / 40) + 2 = 12
endIndex min(startIndex + visibleCount, total) min(30 + 12, 10000) = 42
offsetY (spacer) startIndex × itemHeight (= the inner div's full height sells the scrollbar) 30 × 40 = 1200px
row top i × itemHeight (absolute position inside spacer) row #35 → top = 1400px

1 · the virtual list you write (edit me)

2 · Babel compiles JSX → element tree

<VirtualList/> becomes React.createElement(VirtualList). The loop only pushes visibleCount elements — the other 988 rows never become DOM nodes. On scroll, setScrollTop changes startIndex and React reconciles the new short list.

// (hit "compile & render" to see Babel's output)

3 · live React (windowing, proven)

There are 1,000 rows of data, but only ~12 are ever in the DOM. Scroll the box (or let the gold-check scroll it for you): the DOM nodes badge stays ~12 while Rendered reflects the active window — that gap is the whole point.

DOM nodes (live): / 1000 · gold: count~12 → assert #1 → scroll → assert #201 → re-count~12

intent → pattern

intentpatternwhy
render 10k rows without lag slice to the visible window; absolute-position each row ~12 DOM nodes regardless of total
compute the visible range start=floor(scrollTop/h); count=ceil(vp/h)+buffer O(1) arithmetic — no scan over data
fake the full scroll height inner spacer div: height = total × itemHeight scrollbar looks right; browser owns momentum/inertia
paint a row in its slot position:absolute; top: i × itemHeight no layout shift; each row lands at its exact offset
hide blank flash on fast scroll overscan: render ±2 rows beyond the viewport edges covers the gap while React commits the new slice
variable row heights measure + cache each row's height; recompute cumulative offsets needs a measurement pass — reach for a library here