List Virtualization with react-window in React
List Virtualization with react-window
~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Thanks to pagination, our ProductListPage only ever shows 6 products at once – no performance problem there. But many real lists (chat messages, order history, log entries) should show ALL entries as one scrollable list, not paginated. With thousands of entries, the DOM itself becomes the bottleneck – list virtualization solves exactly that.
Why 5,000 DOM nodes are a problem
A <div> per list item sounds harmless – but with 5,000 items, React creates 5,000+ real DOM nodes at once. The browser has to factor ALL of them into layout, even the ones currently far outside the visible area. That costs memory AND time on the initial render, on every scroll layout update, and on every state change that touches the list.
The idea: only render what's visible
Virtualization renders, at any given moment, ONLY the items currently (plus a small buffer) within the visible viewport – with 5,000 items and a screen height that fits 10, maybe 15 DOM nodes get rendered, not 5,000. As you scroll, items that leave the visible area are removed from the DOM and replaced with new ones – the container still keeps the "correct" total height, so the scrollbar looks right.
Installing react-window
npm install react-windowreact-window is a lean, focused library built exactly for this purpose (the successor to react-virtualized, with a smaller feature set but a considerably smaller bundle size).
A use case: a long order history
To demonstrate virtualization meaningfully, we'll extend AccountPage with a simulated order history of 2,000 entries (in a real app this would come from the server – we generate the data client-side here purely to demonstrate virtualization):
import { FixedSizeList } from 'react-window';
import { useAuthStore } from '../store/authStore';
const ORDER_COUNT = 2000;
const orders = Array.from({ length: ORDER_COUNT }, (_, i) => ({
id: i + 1,
date: new Date(2024, 0, 1 + i).toLocaleDateString('en-US'),
total: (20 + ((i * 7) % 180)).toFixed(2),
}));
function OrderRow({ index, style }) {
const order = orders[index];
return (
<div style={style} className="order-row">
Order #{order.id} – {order.date} – ${order.total}
</div>
);
}
function AccountPage() {
const user = useAuthStore((state) => state.user);
const logout = useAuthStore((state) => state.logout);
return (
<div>
<h2>Welcome back, {user.username}!</h2>
<button onClick={logout}>Log out</button>
<h3>Order History ({ORDER_COUNT} entries)</h3>
<FixedSizeList
height={400}
width="100%"
itemCount={ORDER_COUNT}
itemSize={40}
>
{OrderRow}
</FixedSizeList>
</div>
);
}
export default AccountPage;The four key props of FixedSizeList
height: the visible height of the scroll container in pixels (NOT the height of all the content) – determines how many rows are visible at once.itemCount: the TOTAL number of items – react-window uses this to compute the correct virtual scrollbar height WITHOUT actually rendering every item.itemSize: the fixed height of EACH row in pixels –FixedSizeListassumes ALL rows are the same height (for variable-height rows, there'sVariableSizeList, not covered here).{{OrderRow}}as a child element (NOT a JSX call<OrderRow />!): react-window calls this function component itself and passesindexandstyleas props.
Achtung: style={{style}} on OrderRow is MANDATORY, not optional – react-window uses it to set the absolute positioning (position, top, height) of each row, so it appears at the right spot within the virtual scroll area. Without this style, all rows would stack on top of each other.
Measuring the difference in the profiler
Open the React DevTools profiler from two chapters ago, record the first load of /account. You'll see: only about 10-15 OrderRow instances appear in the flame chart, not 2,000 – and with the browser DevTools' Elements tab, you'll see only a handful of .order-row elements in the actual DOM, no matter how long the list "really" is.
When it's worth the effort
- Lists with HUNDREDS to THOUSANDS of entries meant to be shown all at once (no pagination).
- Chat histories, activity feeds, log viewers, large tables – anywhere "infinite scroll" is wanted.
- NOT needed for our
ProductListPagewith its 6 entries per page – virtualizing such a small list would be pure complexity with no benefit.
Tipp: Rule of thumb: pagination (chapter 24 of "React for Beginners") and virtualization solve RELATED, but different, problems. Pagination reduces how much data even gets loaded from the server in the first place. Virtualization reduces how many DOM nodes exist for ALREADY loaded data. Large apps often combine both: the server delivers, say, 500 entries at once (instead of 6-per-page pagination), the frontend virtualizes their display.