Build a Virtualized Data Table
Requirements
- Handle 100K+ rows smoothly
- Column sorting (client-side for small sets, server-side for large)
- Text filtering and search
- Column resizing and reordering
- Row selection
- Keyboard navigation
Core Concept: Virtualization
Only render rows visible in the viewport + a small overscan buffer:
Total rows: 100,000
Viewport height: 600px
Row height: 40px
Visible rows: 15
Overscan: 5 above + 5 below
Actually rendered: ~25 DOM nodes (not 100,000)Architecture
DataSource â Transform Pipeline â Virtual Window â Rendered Rows
(sort, filter, (visible range (actual DOM)
group) calculation)The Virtual Scroller
function VirtualTable({ data, rowHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const overscan = 5;
const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
const endIndex = Math.min(
data.length,
Math.ceil((scrollTop + containerHeight) / rowHeight) + overscan
);
const visibleRows = data.slice(startIndex, endIndex);
const totalHeight = data.length * rowHeight;
const offsetY = startIndex * rowHeight;
return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleRows.map((row, i) => (
<Row key={startIndex + i} data={row} height={rowHeight} />
))}
</div>
</div>
</div>
);
}Variable Row Heights
When rows have dynamic content, pre-measure or estimate heights. Use a position cache and binary search for scroll-to-index.
Performance Considerations
- Avoid layout thrashing â batch reads and writes
- Use CSS
containâcontain: stricton the scroll container - Debounce scroll events â or use
requestAnimationFrame - Memoize row components â
React.memowith stable keys
The key insight: virtualization trades DOM node count for scroll calculation complexity. The math is cheap; DOM operations are expensive.