Virtualization Pattern: Render Thousands of Rows Without Lag
Use a virtualized list to render only what's visible and keep the DOM lean.
Introduction
Virtualization is a rendering technique where only the items visible in the viewport (and a small buffer) are mounted in the DOM. It lets you handle thousands of rows smoothly without overloading the browser.
Why this matters
Rendering hundreds or thousands of DOM nodes tanks performance. Virtualization libraries render only the rows in the viewport and recycle them as you scroll.
The problem
A regular loop renders all 1,000 items at once—wasted work and slow UI.
Inefficient approach
Rendering every row eagerly bloats the DOM and tanks scroll performance:
const items = Array.from({ length: 1000 }, (_, index) => `Item ${index + 1}`);
function App() {
return (
<div>
<h1>Virtualization Pattern</h1>
<p>Rendering {items.length} items efficiently:</p>
<div style={{ height: 400, overflow: "auto" }}>
{items.map((item, index) => (
<div key={index} style={{ padding: "8px" }}>{item}</div>
))}
</div>
</div>
);
}
export default App;
Problems you’ll feel:
- The browser lays out and paints 1,000 nodes up front.
- Scrolling triggers costly reflows.
- Memory usage spikes, especially on mobile.
The solution
Swap the naive list for a virtualized list. This example uses a List API provided in the lesson code to mirror react‑window behavior.
import { useState, useMemo } from "react";
const items = Array.from({ length: 1000 }, (_, index) => `Item ${index + 1}`);
// Simplified virtualization - only render visible items
function VirtualizedList({ items, containerHeight = 400, itemHeight = 35 }) {
const [scrollTop, setScrollTop] = useState(0);
const visibleRange = useMemo(() => {
const start = Math.floor(scrollTop / itemHeight);
const end = Math.min(
start + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);
return { start, end };
}, [scrollTop, containerHeight, itemHeight, items.length]);
const visibleItems = items.slice(visibleRange.start, visibleRange.end);
const offsetY = visibleRange.start * itemHeight;
return (
<div
style={{
height: containerHeight,
overflow: "auto",
position: "relative"
}}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: items.length * itemHeight, position: "relative" }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, idx) => (
<div
key={visibleRange.start + idx}
style={{ height: itemHeight, padding: "8px" }}
>
{item}
</div>
))}
</div>
</div>
</div>
);
}
function App() {
return (
<div>
<h1>Virtualization Pattern</h1>
<p>Rendering {items.length} items efficiently:</p>
<VirtualizedList items={items} />
</div>
);
}
export default App;
Step-by-step
- Generate a large
itemsarray for demonstration. - Create a
Rowcomponent that rendersitems[index]and appliesstyle. - Replace the naive loop with a virtualized
<List>component. - Set a fixed
heightand consistentrowHeightto enable virtualization. - Verify smooth scrolling and minimal DOM nodes in DevTools.
Tips
- With react‑window, prefer
FixedSizeListwithitemCount,itemSize, anditemData. - Keep row components pure and cheap—avoid expensive calculations inside the renderer.
- Give the list a fixed height; virtualization needs a viewport to work.
Knowledge base
Problem snippet:
<div style={{ height: 400 }}>
{items.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
Solution snippet:
<List
height={400}
rowCount={items.length}
rowHeight={35}
rowComponent={Row}
rowProps={{ items }}
/>