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.
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.
Build It
Code Examples
A from-scratch fixed-height virtualized list
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
- 1Extend the implementation to support variable row heights using a running offset cache instead of index * rowHeight.
- 2Add horizontal virtualization for a wide table with many columns, reusing the same startIndex/endIndex math on the X axis.
- 3Benchmark real DOM node count and scroll FPS for 100,000 items with vs without virtualization.