List Virtualization: Render Large Lists Efficiently
List virtualization is essential for rendering large datasets efficiently. Master this technique for handling thousands of items.
Quick Navigation: Basic Virtualization • Variable Size Lists • Grid Virtualization • Infinite Scroll with Virtualization • Advanced Patterns • Best Practices
Quick Decision Guide
Quick Implementation Guide:
react-window: Lightweight library for list virtualization. Use FixedSizeList for equal-height items, VariableSizeList for dynamic heights.
How it works: Renders only visible items + small buffer. Calculates scroll position to show correct items.
Performance gain: Rendering 10,000 items → ~20 DOM nodes instead of 10,000. 100x performance improvement.
Best Practice: Use for lists with 100+ items. Provide accurate item heights for smooth scrolling. Add overscan for better UX.
Basic Virtualization
Fixed Size List
import { FixedSizeList } from 'react-window';
function VirtualList({ data }) {
const Row = ({ index, style }) => (
<div style={style} className="list-item">
<h3>{data[index].title}</h3>
<p>{data[index].description}</p>
</div>
);
return (
<FixedSizeList
height={600} // Container height
itemCount={data.length}
itemSize={80} // Each item is 80px tall
width="100%"
overscanCount={5} // Render 5 extra items for smooth scrolling
>
{Row}
</FixedSizeList>
);
}How It Works
Viewport (600px tall):
Visible: Items 0-7 (8 items × 75px)
Buffer (overscan): Items 8-12 (5 extra)
Total rendered: 13 items (not 10,000!)
User scrolls:
New visible: Items 10-17
New buffer: Items 8-9, 18-22
Items 0-7 are unmounted, 10-17 mountedPerformance Comparison
Traditional (10,000 items):
- DOM nodes: 10,000
- Initial render: 5.2s
- Memory: 250MB
- Scroll FPS: 15fps
Virtualized (10,000 items):
- DOM nodes: ~15
- Initial render: 0.05s (100x faster!)
- Memory: 3MB
- Scroll FPS: 60fpsVariable Size Lists
Dynamic Heights
import { VariableSizeList } from 'react-window';
function DynamicList({ data }) {
const listRef = useRef(null);
// Cache item heights
const getItemSize = (index) => {
// Short items: 60px, long items: 120px
return data[index].description.length > 100 ? 120 : 60;
};
const Row = ({ index, style }) => (
<div style={style} className="list-item">
<h3>{data[index].title}</h3>
<p>{data[index].description}</p>
</div>
);
return (
<VariableSizeList
ref={listRef}
height={600}
itemCount={data.length}
itemSize={getItemSize} // Function, not fixed number
width="100%"
estimatedItemSize={80} // Average height for calculations
overscanCount={5}
>
{Row}
</VariableSizeList>
);
}Reset Cache on Data Change
function DynamicList({ data }) {
const listRef = useRef(null);
// Reset cached heights when data changes
useEffect(() => {
listRef.current?.resetAfterIndex(0);
}, [data]);
// ... rest of component
}Measuring Actual Heights
const Row = ({ index, style, data }) => {
const rowRef = useRef(null);
useEffect(() => {
if (rowRef.current) {
const height = rowRef.current.getBoundingClientRect().height;
// Update cache if actual height differs
if (height !== style.height) {
data.setSize(index, height);
}
}
}, [index, data, style.height]);
return (
<div ref={rowRef} style={style}>
{/* Content with unknown height */}
</div>
);
};Grid Virtualization
Fixed Size Grid
import { FixedSizeGrid } from 'react-window';
function VirtualGrid({ data, columnCount }) {
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * columnCount + columnIndex;
const item = data[index];
return (
<div style={style} className="grid-cell">
<img src={item.image} alt={item.title} />
<p>{item.title}</p>
</div>
);
};
return (
<FixedSizeGrid
columnCount={columnCount}
columnWidth={200}
height={600}
rowCount={Math.ceil(data.length / columnCount)}
rowHeight={250}
width={800}
>
{Cell}
</FixedSizeGrid>
);
}Responsive Grid
function ResponsiveGrid({ data }) {
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
const columnCount = Math.floor(dimensions.width / 200);
useEffect(() => {
const handleResize = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight - 100
});
};
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<FixedSizeGrid
columnCount={columnCount}
columnWidth={200}
height={dimensions.height}
rowCount={Math.ceil(data.length / columnCount)}
rowHeight={250}
width={dimensions.width}
>
{Cell}
</FixedSizeGrid>
);
}Infinite Scroll with Virtualization
Load More on Scroll
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
function InfiniteList({ loadMore, hasNextPage, items }) {
const itemCount = hasNextPage ? items.length + 1 : items.length;
const isItemLoaded = (index) => !hasNextPage || index < items.length;
const loadMoreItems = () => {
if (!hasNextPage) return Promise.resolve();
return loadMore();
};
const Row = ({ index, style }) => {
if (!isItemLoaded(index)) {
return <div style={style}>Loading...</div>;
}
return (
<div style={style}>
{items[index].name}
</div>
);
};
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
threshold={15} // Load more when 15 items from end
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={itemCount}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
width="100%"
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
);
}Usage with API
function App() {
const [items, setItems] = useState([]);
const [hasNextPage, setHasNextPage] = useState(true);
const [page, setPage] = useState(0);
const loadMore = async () => {
const response = await fetch(`/api/items?page=${page}`);
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
setPage(prev => prev + 1);
setHasNextPage(newItems.length > 0);
};
useEffect(() => {
loadMore();
}, []);
return (
<InfiniteList
items={items}
loadMore={loadMore}
hasNextPage={hasNextPage}
/>
);
}Advanced Patterns
Scrolling to Item
function ScrollableList({ data, initialScrollIndex }) {
const listRef = useRef(null);
const scrollToItem = (index) => {
listRef.current?.scrollToItem(index, 'center');
};
return (
<>
<button onClick={() => scrollToItem(500)}>
Jump to item 500
</button>
<FixedSizeList
ref={listRef}
height={600}
itemCount={data.length}
itemSize={50}
initialScrollOffset={initialScrollIndex * 50}
width="100%"
>
{Row}
</FixedSizeList>
</>
);
}Item Data & Context
function List({ data, onItemClick }) {
// Pass data to all rows without re-rendering
const itemData = useMemo(() => ({
items: data,
onClick: onItemClick
}), [data, onItemClick]);
const Row = memo(({ index, style, data }) => {
const { items, onClick } = data;
const item = items[index];
return (
<div
style={style}
onClick={() => onClick(item)}
>
{item.name}
</div>
);
});
return (
<FixedSizeList
height={600}
itemCount={data.length}
itemSize={50}
itemData={itemData} // Passed to Row component
width="100%"
>
{Row}
</FixedSizeList>
);
}Custom Scrollbar
function CustomScrollList({ data }) {
return (
<FixedSizeList
height={600}
itemCount={data.length}
itemSize={50}
width="100%"
style={{
scrollbarWidth: 'thin',
scrollbarColor: '#888 #f1f1f1'
}}
className="custom-scrollbar"
>
{Row}
</FixedSizeList>
);
}
// CSScss
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
```
Best Practices
When to Virtualize
✅ Virtualize When:
❌ Don't Virtualize When:
Performance Tips
// 1. Memoize row components
const Row = memo(({ index, style, data }) => {
return <div style={style}>{data[index].name}</div>;
});
// 2. Use stable keys
<FixedSizeList
itemKey={(index, data) => data[index].id} // Use stable ID
>
{Row}
</FixedSizeList>
// 3. Optimize renders
const itemData = useMemo(
() => ({ items, onItemClick }),
[items, onItemClick]
);
// 4. Proper overscan
<FixedSizeList
overscanCount={5} // Balance between smoothness and performance
>
{Row}
</FixedSizeList>Accessibility
// Add ARIA attributes
function AccessibleList({ data }) {
return (
<FixedSizeList
height={600}
itemCount={data.length}
itemSize={50}
width="100%"
role="list"
aria-label="Virtualized list"
>
{({ index, style }) => (
<div
style={style}
role="listitem"
aria-setsize={data.length}
aria-posinset={index + 1}
>
{data[index].name}
</div>
)}
</FixedSizeList>
);
}Testing
// Test with large datasets
const testData = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`
}));
// Measure performance
console.time('render');
render(<VirtualList data={testData} />);
console.timeEnd('render');
// Expected: < 50ms