Virtualizing Large Lists in React with react-window
Learn how to render thousands of items efficiently using react-window for list and grid virtualization in React applications.
What you'll learn
- ✓Why virtualization matters for large lists
- ✓Setting up react-window with FixedSizeList
- ✓Variable-height rows with VariableSizeList
- ✓Grid virtualization for two-dimensional data
- ✓Integrating with infinite scroll and search
Prerequisites
- •Basic React knowledge
- •Familiarity with component props and rendering
Rendering a list of 10,000 items means creating 10,000 DOM nodes. The browser slows down, scrolling stutters, and memory usage spikes. Virtualization solves this by only rendering the items currently visible in the viewport, plus a small buffer above and below. The react-window library makes this straightforward.
Why Not Just Render Everything
Every DOM node costs memory and layout computation. A list of 1,000 simple <div> elements might be fine, but add images, event listeners, or nested components and performance degrades quickly. Virtualization keeps the DOM node count constant regardless of list size. Scroll through 100 items or 100,000, and the browser always manages the same small set of nodes.
Installing react-window
npm install react-window
The library is small (about 6 KB gzipped) and has no dependencies.
FixedSizeList: Uniform Row Heights
When every row has the same height, use FixedSizeList. It is the simplest and most performant option.
import { FixedSizeList } from 'react-window';
const users = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
}));
function Row({ index, style }) {
const user = users[index];
return (
<div style={style} className="row">
<span className="name">{user.name}</span>
<span className="email">{user.email}</span>
</div>
);
}
function UserList() {
return (
<FixedSizeList
height={600}
width="100%"
itemCount={users.length}
itemSize={50}
>
{Row}
</FixedSizeList>
);
}
The style prop is critical. It positions each row absolutely within the scrollable container. Never override the top or height properties in the style, or the layout will break.
The height prop sets the viewport height. The itemSize prop is the fixed height of each row in pixels. react-window calculates the total scrollable height and only renders the rows currently visible.
Passing Data Through itemData
Avoid closing over external arrays in the Row component. Instead, use the itemData prop to pass data explicitly. This enables memoization.
import { FixedSizeList } from 'react-window';
import { memo } from 'react';
const Row = memo(function Row({ index, style, data }) {
const user = data[index];
return (
<div style={style} className="row">
<strong>{user.name}</strong>
<span>{user.email}</span>
</div>
);
});
function UserList({ users }) {
return (
<FixedSizeList
height={600}
width="100%"
itemCount={users.length}
itemSize={50}
itemData={users}
>
{Row}
</FixedSizeList>
);
}
Wrapping Row in memo prevents re-renders when the parent updates but the row data has not changed.
VariableSizeList: Dynamic Row Heights
When rows have different heights, use VariableSizeList. You provide a function that returns the height for each index.
import { VariableSizeList } from 'react-window';
const messages = [
{ id: 1, text: 'Hello' },
{ id: 2, text: 'This is a longer message that wraps to multiple lines and takes more vertical space in the UI.' },
{ id: 3, text: 'Short' },
// ... thousands more
];
function getItemSize(index) {
// Estimate height based on text length
const text = messages[index].text;
const lineCount = Math.ceil(text.length / 60);
return 24 + lineCount * 20; // base padding + lines
}
function MessageRow({ index, style }) {
return (
<div style={style} className="message">
{messages[index].text}
</div>
);
}
function MessageList() {
return (
<VariableSizeList
height={500}
width="100%"
itemCount={messages.length}
itemSize={getItemSize}
>
{MessageRow}
</VariableSizeList>
);
}
If the height depends on rendered content (which you cannot know ahead of time), you may need to measure rows dynamically. The react-virtualized-auto-sizer and react-window communities offer solutions for this, though it adds complexity.
FixedSizeGrid: Two-Dimensional Virtualization
For grids, FixedSizeGrid virtualizes both rows and columns.
import { FixedSizeGrid } from 'react-window';
function Cell({ columnIndex, rowIndex, style }) {
return (
<div style={style} className="cell">
Row {rowIndex}, Col {columnIndex}
</div>
);
}
function DataGrid() {
return (
<FixedSizeGrid
columnCount={50}
columnWidth={150}
height={600}
rowCount={1000}
rowHeight={40}
width={800}
>
{Cell}
</FixedSizeGrid>
);
}
This renders a spreadsheet-like view where only the visible cells exist in the DOM. Scrolling horizontally and vertically is smooth even with 50,000 cells.
Auto-Sizing the Container
Hard-coding the list height is fragile. Use react-virtualized-auto-sizer to make the list fill its parent container.
npm install react-virtualized-auto-sizer
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
function UserList({ users }) {
return (
<div style={{ height: '100%', width: '100%' }}>
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
height={height}
width={width}
itemCount={users.length}
itemSize={50}
itemData={users}
>
{Row}
</FixedSizeList>
)}
</AutoSizer>
</div>
);
}
The parent element must have a defined height (via CSS or flex layout) for AutoSizer to work.
Infinite Scroll with react-window-infinite-loader
To load more data as the user scrolls, combine react-window with react-window-infinite-loader.
npm install react-window-infinite-loader
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
function InfiniteList({ items, hasMore, loadMore }) {
const itemCount = hasMore ? items.length + 1 : items.length;
const isItemLoaded = (index) => !hasMore || index < items.length;
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMore}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
ref={ref}
height={600}
width="100%"
itemCount={itemCount}
itemSize={50}
onItemsRendered={onItemsRendered}
>
{({ index, style }) => (
<div style={style}>
{isItemLoaded(index) ? items[index].name : 'Loading...'}
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
The loader calls loadMore when the user scrolls near unloaded items.
When to Virtualize
Virtualize when you have more than a few hundred items with complex row components, or more than a few thousand items with simple rows. For shorter lists, the overhead of virtualization (absolute positioning, scroll handling) is not worth it. Measure first: if the list scrolls smoothly without virtualization, keep it simple.
Related articles
- React React Performance: Memo, useMemo, and useCallback Guide
Optimize React performance with React.memo, useMemo, and useCallback. Learn when to use each, how to profile, and avoid common pitfalls.
- React React useTransition: Advanced Patterns for Responsive UIs
Go beyond basic useTransition usage with patterns for search, navigation, tab switching, and priority-based rendering in React concurrent mode.
- React React Compiler and Automatic Memoization
Understand how the React Compiler works, what automatic memoization means, and how it eliminates the need for useMemo, useCallback, and React.memo.
- React React Concurrent Features: useTransition, useDeferredValue, and More
Master React's concurrent rendering features including useTransition, useDeferredValue, and strategies for keeping your UI responsive.