RoadmapDay 57 / 80
React (Web)Month 3 · Week 12

Day 57: Build: Virtualized List Component from Scratch

Implement the core virtualization algorithm by hand — no library — to fully internalize what FlatList/FlashList/react-window do under the hood.

Mark this day complete

Study

Concepts

The core algorithm: math, not magic

Given a fixed row height and a scroll container, you can compute exactly which row indices are visible with pure arithmetic: `startIndex = Math.floor(scrollTop / rowHeight)`, `endIndex = Math.ceil((scrollTop + containerHeight) / rowHeight)`. Render ONLY that index range (plus a small overscan buffer above/below for smoother fast-scrolling), and use a tall spacer element (`totalHeight = itemCount * rowHeight`) so the scrollbar's size/position is correct even though most rows are never actually in the DOM.

Absolutely positioning each rendered row at `top: index * rowHeight` inside that spacer container (rather than relying on normal document flow) is what lets you render an arbitrary slice of indices without needing every prior row present to determine layout position.

See It

Visualizations

Visualization

Visible window computed from scrollTop

rowHeight=50, containerHeight=300 → ~6 rows visible, computed by pure math, not measurement.

rows 0-19 (above, virtual)
rows 20-25 (rendered)
rows 26-999 (below, virtual)
not in DOM
REAL DOM NODES
not in DOM

Build It

Code Examples

A from-scratch fixed-height virtualized list

jsx
function VirtualList({ items, rowHeight, containerHeight, renderRow }) {
  const [scrollTop, setScrollTop] = useState(0);
  const overscan = 3;

  const totalHeight = items.length * rowHeight;
  const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
  const endIndex = Math.min(
    items.length - 1,
    Math.ceil((scrollTop + containerHeight) / rowHeight) + overscan
  );

  const visibleItems = [];
  for (let i = startIndex; i <= endIndex; i++) {
    visibleItems.push(
      <div
        key={items[i].id}
        style={{ position: 'absolute', top: i * rowHeight, height: rowHeight, width: '100%' }}
      >
        {renderRow(items[i], i)}
      </div>
    );
  }

  return (
    <div
      onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
      style={{ height: containerHeight, overflowY: 'auto', position: 'relative' }}
    >
      {/* Spacer gives the scrollbar the CORRECT total size */}
      <div style={{ height: totalHeight, position: 'relative' }}>
        {visibleItems}
      </div>
    </div>
  );
}

// Usage: renders only ~12 real DOM nodes for a 50,000-item list
<VirtualList
  items={rows}
  rowHeight={50}
  containerHeight={300}
  renderRow={(item) => <span>{item.label}</span>}
/>

Remember

Key Takeaways

  • startIndex/endIndex are pure arithmetic from scrollTop and rowHeight — no measuring pass needed for fixed-height rows.
  • A tall spacer element sized to totalHeight gives the scrollbar correct size/position despite most rows never being in the DOM.
  • Absolute positioning each row lets you render an arbitrary index slice without needing prior siblings for layout.
  • Overscan (rendering a few extra rows beyond the visible window) smooths out fast scrolling by staying ahead of the viewport.
  • This is precisely the algorithm underneath FlatList, react-window, and FlashList's baseline behavior — building it once demystifies all of them.

Do It

Practice

  1. 1Extend the implementation to support variable row heights using a running offset cache instead of index * rowHeight.
  2. 2Add horizontal virtualization for a wide table with many columns, reusing the same startIndex/endIndex math on the X axis.
  3. 3Benchmark real DOM node count and scroll FPS for 100,000 items with vs without virtualization.