Handling Large Lists with Virtualization in React
Learn how to efficiently render thousands of list items using virtualization techniques, improving performance and user experience.
- React
- Performance
- Virtualization
- TypeScript
- Lists
Rendering large lists in React can quickly become a performance bottleneck. When you have hundreds or thousands of items, rendering them all at once causes significant lag, memory issues, and poor user experience. Virtualization solves this by rendering only the visible items plus a small buffer.
The Problem with Large Lists
Consider a list with 10,000 items. Rendering all of them means:
- Creating 10,000 DOM nodes
- High memory consumption
- Slow initial render
- Laggy scrolling
- Poor mobile performance
interface Item {
id: string;
}
interface BadListProps {
items: Item[];
}
// This will be slow with large datasets
function BadList({ items }: BadListProps) {
return (
<div>
{items.map(item => (
<ListItem key={item.id} item={item} />
))}
</div>
);
}
What is Virtualization?
Virtualization (also called windowing) renders only the items visible in the viewport plus a small buffer. As you scroll, items are dynamically added and removed from the DOM.
Using react-window
react-window is a lightweight, efficient virtualization library. Here's how to use it:
Basic Fixed Size List
import { FixedSizeList } from 'react-window';
interface Item {
id: string;
}
interface VirtualizedListProps {
items: Item[];
}
function VirtualizedList({ items }: VirtualizedListProps) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<ListItem item={items[index]} />
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Variable Size List
When items have different heights, use VariableSizeList:
import { VariableSizeList } from 'react-window';
interface Item {
id: string;
isExpanded: boolean;
}
interface VariableHeightListProps {
items: Item[];
}
function VariableHeightList({ items }: VariableHeightListProps) {
const getItemSize = (index: number): number => {
// Return height based on item content
return items[index].isExpanded ? 100 : 50;
};
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<ListItem item={items[index]} />
</div>
);
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
Using react-virtual
react-virtual provides more flexibility and hooks-based API:
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
interface Item {
id: string;
}
interface VirtualizedListProps {
items: Item[];
}
function VirtualizedList({ items }: VirtualizedListProps) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5, // Render 5 extra items outside viewport
});
return (
<div
ref={parentRef}
style={{ height: '600px', overflow: 'auto' }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ListItem item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
Grid Virtualization
For two-dimensional lists (grids), use FixedSizeGrid:
import { FixedSizeGrid } from 'react-window';
interface Item {
id: string;
}
interface VirtualizedGridProps {
items: Item[];
}
function VirtualizedGrid({ items }: VirtualizedGridProps) {
const Cell = ({
columnIndex,
rowIndex,
style
}: {
columnIndex: number;
rowIndex: number;
style: React.CSSProperties;
}) => (
<div style={style}>
<GridItem item={items[rowIndex * 5 + columnIndex]} />
</div>
);
return (
<FixedSizeGrid
columnCount={5}
columnWidth={200}
height={600}
rowCount={Math.ceil(items.length / 5)}
rowHeight={150}
width={1000}
>
{Cell}
</FixedSizeGrid>
);
}
Advanced Patterns
Dynamic Height Calculation
When item heights are unknown, measure them dynamically:
import { useState, useRef, useEffect } from 'react';
import { VariableSizeList } from 'react-window';
interface Item {
id: string;
}
interface DynamicHeightListProps {
items: Item[];
}
function DynamicHeightList({ items }: DynamicHeightListProps) {
const [heights, setHeights] = useState<Record<number, number>>({});
const listRef = useRef<VariableSizeList>(null);
const getItemSize = (index: number): number => heights[index] || 50;
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const rowRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (rowRef.current) {
const height = rowRef.current.getBoundingClientRect().height;
setHeights(prev => ({ ...prev, [index]: height }));
}
}, [index]);
return (
<div ref={rowRef} style={style}>
<ListItem item={items[index]} />
</div>
);
};
return (
<VariableSizeList
ref={listRef}
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
Infinite Scrolling with Virtualization
Combine virtualization with infinite scrolling:
import { useState, useRef } from 'react';
import { FixedSizeList } from 'react-window';
interface Item {
id: string;
}
function InfiniteVirtualizedList() {
const [items, setItems] = useState<Item[]>([]);
const [hasMore, setHasMore] = useState<boolean>(true);
const listRef = useRef<FixedSizeList>(null);
const loadMore = async (): Promise<void> => {
const newItems = await fetchMoreItems();
setItems(prev => [...prev, ...newItems]);
setHasMore(newItems.length > 0);
};
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
if (index === items.length - 1 && hasMore) {
loadMore();
}
return (
<div style={style}>
<ListItem item={items[index]} />
</div>
);
};
return (
<FixedSizeList
ref={listRef}
height={600}
itemCount={hasMore ? items.length + 1 : items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Performance Considerations
Overscan
Overscan determines how many items to render outside the viewport. More overscan means smoother scrolling but more DOM nodes:
<FixedSizeList
overscanCount={5} // Render 5 items above and below viewport
// ... other props
/>
Memoization
Memoize your row component to prevent unnecessary re-renders:
interface RowProps {
index: number;
style: React.CSSProperties;
data: Item[];
}
const Row = React.memo<RowProps>(({ index, style, data }) => (
<div style={style}>
<ListItem item={data[index]} />
</div>
));
Key Extraction
Ensure stable keys for list items:
// Good: Use unique, stable IDs
items.map(item => <ListItem key={item.id} />)
// Bad: Using index as key
items.map((item, index) => <ListItem key={index} />)
When to Use Virtualization
Use virtualization when:
- You have 100+ items
- Items have complex rendering logic
- Performance is critical
- Mobile performance matters
Don't use virtualization when:
- You have fewer than 50 items
- Items are extremely simple
- You need CSS scroll snap
- You need native browser scrolling behavior
Real-World Performance
In production applications, virtualization has resulted in:
- 95% reduction in initial render time for 10,000+ item lists
- 90% reduction in memory usage
- Smooth 60fps scrolling regardless of list size
- Improved mobile performance significantly
Best Practices
- Measure item heights: Use
estimateSizeaccurately - Optimize overscan: Balance smoothness vs. performance
- Memoize row components: Prevent unnecessary re-renders
- Use stable keys: Ensure React can track items efficiently
- Handle dynamic content: Account for content that changes size
- Test on mobile: Virtualization is especially important on mobile devices
Virtualization is essential for building performant React applications that handle large datasets. Choose the right library for your needs and implement it correctly to see dramatic performance improvements.
Keep reading
Performance Optimization in React: A Practical Guide
Learn practical techniques to optimize React applications, from code splitting to memoization strategies and rendering optimizations.
React 19: What's New and Why It Matters
Explore the groundbreaking features in React 19, including Actions, useOptimistic, and the new compiler that transforms how we build React applications.
State Management with Zustand: A Modern Approach
Learn how to use Zustand for lightweight, performant state management in React applications without the boilerplate of Redux.