react-window: The Complete Guide
to Virtualized Lists in React
Rendering 10,000 rows in a React app without melting the browser tab is not
magic — it’s list virtualization. This guide walks you
through everything: installation, FixedSizeList,
VariableSizeList, scroll performance tuning, and the edge cases
nobody else talks about.
⏱ 18 min read
⚙️ react-window 1.8.x · React 18+
🎯 Intermediate – Advanced
🗂 Semantic Core
Keyword clusters used to structure this article for maximum search relevance and topical authority.
- react-window
- React window virtualization
- react-window FixedSizeList
- react-window VariableSizeList
- react-window tutorial
- React virtualized list
- React large list rendering
- React performance optimization
- React scroll performance
- React list component
- React infinite scroll
- windowing technique React
- render only visible items
- react-window npm install
- overscanCount prop
- itemSize prop
- lazy rendering React
- react-window-infinite-loader
⚡ Why Virtualization Exists (and Why You Need It)
There’s a quiet moment of horror every React developer has experienced at
least once: you map over an array of 5,000 objects, render a list, and
watch the browser choke. The scroll stutters. The tab hangs. The user
leaves. The problem isn’t React — React is perfectly good at updating the
DOM. The problem is that you’re asking it to create, style, and manage
five thousand DOM nodes simultaneously, most of which are
completely invisible to the user. That’s simply too much work for any
browser to handle gracefully.
React window virtualization
solves this with a technique called windowing: instead of
rendering every item in a list, only the items currently visible in the
viewport (plus a small overscan buffer) are mounted in the DOM. As the user
scrolls, items entering the view are rendered and items leaving it are
unmounted. The user perceives a perfectly normal, continuous list. The
browser is only managing a handful of actual DOM nodes at any given moment.
It’s one of the highest-ROI performance techniques in the React ecosystem.
The library that makes this effortless is
react-window,
written by Brian Vaughn (formerly on the React core team). It’s a focused,
minimal implementation of the windowing pattern, covering the majority of
real-world use cases with an API that takes about ten minutes to learn. If
you’ve heard of
react-virtualized
— react-window is its leaner successor. Same author, same core idea,
smaller bundle and cleaner API. For most projects, react-window is the
correct choice.
| Approach | DOM Nodes (10k items) | Initial Render | Scroll FPS | Memory |
|---|---|---|---|---|
Naive Array.map() |
10,000+ | ~2,400ms | ~12 fps | High |
| Pagination | ~50–200 | ~180ms | 60 fps | Medium |
| react-window | ~15–40 | ~60ms | 60 fps | Low |
Your mileage will vary based on item complexity and row height, but the
directional difference is consistent: react-window keeps DOM
nodes in the tens, not the thousands.
📦 Installation & react-window Setup
react-window installation
is a one-liner. The library has no mandatory peer dependencies beyond React
and React DOM themselves, which means you’re not dragging in half of npm
along with it. At roughly 6KB gzipped, it’ll have a negligible impact on
your bundle size.
react-window setup · step 1
# npm npm install react-window # yarn yarn add react-window # pnpm pnpm add react-window
That’s it. No additional configuration, no Webpack plugin, no PostCSS.
The package ships with TypeScript types out of the box (via
@types/react-window, which you may need to install separately
in older setups, but most modern toolchains will prompt you). Once
installed, you have access to four core components:
FixedSizeList, VariableSizeList,
FixedSizeGrid, and VariableSizeGrid. This guide
focuses on the list variants, which cover the overwhelming majority of
real-world use cases.
Before writing any JSX, there’s one mental model to internalize:
react-window doesn’t scroll your page — it scrolls an internal
container. The list component renders a fixed-height (or
fixed-width for horizontal lists) container. This container has
overflow: auto and a calculated inner height tall enough to
represent all items. Only the visible subset of items are actually
mounted. Everything else is an illusion maintained by precise
transform: translateY() positioning. Understanding this helps
you avoid the most common pitfalls in
styling and layout.
Basic import structure
import { FixedSizeList, VariableSizeList } from 'react-window'; // Optional but highly recommended for responsive sizing: import AutoSizer from 'react-virtualized-auto-sizer';
react-virtualized-auto-sizeralongside react-window. It’s a tiny utility that provides the container’s
width and height to your list, making it trivially easy to build
responsive virtualized lists without hardcoding pixel dimensions.
📐 FixedSizeList: The Workhorse
react-window FixedSizeList
is the component you’ll reach for in most situations. Its defining
characteristic is that every row has the same pixel height. This
constraint allows react-window to perform virtually instant calculations:
to know where item #847 starts, it simply multiplies 847 by the row height.
No measurement loops, no layout recalculation, no tears.
The required props are: height (the visible container height),
itemCount (total number of items), itemSize
(the fixed height of each row in pixels), and width (the
container width). You then pass a render function as
children — not an array of JSX, but a function that
receives { index, style } and returns a single row. The
style prop is mandatory and must be applied to the outermost
element of your row — it carries the absolute positioning that makes the
windowing illusion work.
FixedSizeList · complete react-window example
import { FixedSizeList as List } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; const items = Array.from({ length: 10_000 }, (_, i) => ({ id: i, name: `Item #${i + 1}`, value: Math.random().toFixed(4), })); /** Row renderer — must accept { index, style } */ const Row = ({ index, style }) => (style={{ ...style, display: 'flex', alignItems: 'center', padding: '0 16px', borderBottom: '1px solid #2e3348', background: index % 2 === 0 ? '#1a1d27' : '#22263a', }} > style={{ fontWeight: 600, color: '#61dafb' }}> {items[index].name} style={{ marginLeft: 'auto', color: '#8892a4' }}> {items[index].value}); export default function VirtualizedList() { return (style={{ height: '600px', border: '1px solid #2e3348' }}>); }{({ height, width }) => ( height={height} itemCount={items.length} itemSize={52} // fixed row height in px width={width} overscanCount={5} // render 5 extra rows outside viewport > {Row}
)}
A few things to notice in the example above. First, the Row
component is defined outside of the parent component. This is
critical. If you define the row renderer inline inside the parent, React
will treat it as a new function reference on every render, causing the
list to unmount and remount every visible row unnecessarily — completely
defeating the purpose. Second, the spread ...style always
comes first, so your custom styles extend rather than override
the positioning applied by react-window. Override the style.top
or style.position props and the list will visually break in
ways that are deeply confusing to debug.
Passing Data to Row Renderers
The render function only receives index and style.
So how do you pass additional data (the actual list items, event handlers,
selected state) into each row? The answer is the
itemData prop. Whatever you pass to itemData
becomes available in your row renderer as data. This is the
idiomatic react-window pattern and it’s important for performance: passing
data via itemData instead of closure keeps the row renderer
memoizable.
itemData pattern — passing props to rows
const Row = ({ index, style, data }) => { const { items, onSelect, selectedId } = data; const item = items[index]; return (style={{ ...style, cursor: 'pointer', background: item.id === selectedId ? '#1e3a5f' : 'transparent' }} onClick={() => onSelect(item.id)} > {item.name}); }; // In your parent component:itemData={{ items, onSelect: handleSelect, selectedId }} /* ...other props */ > {Row}
Combine itemData with React.memo on your row
component and you have a genuinely efficient virtualized list. React will
skip re-rendering rows that haven’t changed, react-window will skip
mounting rows outside the viewport, and the browser will stop sweating.
This combination — FixedSizeList + itemData +
React.memo — is the foundation of nearly every performant
React large list rendering
pattern you’ll find in production codebases.
📏 VariableSizeList: Handling Dynamic Heights
The real world is inconveniently full of lists where items don’t have
uniform heights: comment threads, chat messages, product cards with varying
description lengths, news feeds. For these scenarios,
react-window VariableSizeList is the right
tool. It works the same way as FixedSizeList but instead of a
number, itemSize accepts a function that takes an
index and returns the height of that specific item.
VariableSizeList · basic react-window example
import { VariableSizeList as List } from 'react-window'; // Pre-computed heights for each item const itemHeights = items.map(item => item.expanded ? 120 : item.hasImage ? 80 : 48 ); const getItemSize = (index) => itemHeights[index];height={600} itemCount={items.length} itemSize={getItemSize} // function, not a number width={'100%'} estimatedItemSize={60} // helps with scrollbar accuracy > {Row}
There’s a subtlety here that trips up most developers the first time:
VariableSizeList caches the sizes it receives. If item
heights change at runtime (say, the user expands an accordion), you need
to explicitly tell the list to invalidate its cache by calling
listRef.current.resetAfterIndex(index). Without this call,
the list will render items with stale position offsets, and your layout
will look like it was assembled during an earthquake.
Invalidating cache after height change
import { useRef, useCallback } from 'react'; import { VariableSizeList } from 'react-window'; function AccordionList({ items }) { const listRef = useRef(null); const [expandedIndex, setExpandedIndex] = useState(null); const getSize = useCallback( (i) => i === expandedIndex ? 200 : 52, [expandedIndex] ); const toggleItem = useCallback((index) => { setExpandedIndex(prev => prev === index ? null : index); // Reset from this index downward listRef.current?.resetAfterIndex(index); }, []); return (ref={listRef} height={500} itemCount={items.length} itemSize={getSize} width={'100%'} itemData={{ items, expandedIndex, onToggle: toggleItem }} > {AccordionRow} ); }
The estimatedItemSize prop deserves special attention. It
doesn’t affect rendering correctness, but it does affect scrollbar
accuracy. Without it, the scrollbar thumb size and position may jump
erratically as new items are measured and the total height estimate
updates. Set it to a reasonable average height for your data. If your
items have genuinely unpredictable heights (because they’re determined by
rendered content), consider the
CellMeasurer pattern
from the react-virtualized ecosystem, or a custom
ResizeObserver-based solution to measure items after mount.
🚀 Squeezing More Performance Out of react-window
Using react-window
already delivers enormous React performance
optimization benefits out of the box. But there are several
additional techniques that push
React scroll performance to the theoretical
maximum, particularly when your row components are non-trivial.
Memoize Everything That Moves
Wrap row components in React.memo and stabilize all
functions you pass via itemData with useCallback.
Wrap the itemData object itself in useMemo so it
doesn’t produce a new reference on every parent render. This sounds tedious
but it’s the difference between 60fps scroll and 30fps scroll when rows
contain images, charts, or anything computationally heavier than a
Memoization pattern for react-window rows
import { memo, useMemo, useCallback } from 'react'; // ✅ Memoized row component const Row = memo(({ index, style, data }) => { const { items, onSelect } = data; return (style={style} onClick={() => onSelect(items[index].id)}> {items[index].name}); }); function ParentComponent({ rawItems }) { // ✅ Stable function reference const handleSelect = useCallback((id) => { console.log('selected:', id); }, []); // ✅ Stable data object — won't trigger row re-renders const itemData = useMemo( () => ({ items: rawItems, onSelect: handleSelect }), [rawItems, handleSelect] ); return (itemData={itemData} {/* ...props */}> {Row} ); }
Tuning overscanCount
The overscanCount prop controls how many rows beyond the
visible area react-window pre-renders. The default is 1–2. Increasing
it reduces the “flash of empty content” you can sometimes see on very
fast scrolls at the cost of rendering more nodes. On modern hardware,
an overscanCount of 3–5 is a good sweet spot. Going higher
than 10 starts to approach the “render everything anyway” antipattern and
defeats the purpose.
A counterintuitive finding: for very complex row components (think rows
with lazy-loaded images or canvas elements), a lower
overscanCount can sometimes feel smoother. This is because
each new mount triggers layout and paint work. If you’re mounting 10
complex rows simultaneously on every scroll event, the main thread will
stall. Reducing to 2–3 overscan rows means fewer simultaneous mounts,
fewer layout thrashes, smoother scroll. Profile first with React DevTools
Profiler and Chrome’s Performance tab before tuning blindly.
The techniques in this section directly address the “React scroll
performance” and “React large list rendering” search intents. Users
searching these terms are typically mid-debugging and need actionable,
specific solutions — not introductory theory.
Avoid Inline Styles on Rows
This one is subtle. The style prop react-window gives you
must be applied as-is, but your own additional styles should
come from CSS classes rather than inline style objects. Every inline style
object is a new JavaScript object allocation. In a fast scroll, React
processes hundreds of row renders per second. Creating fresh style objects
for each puts pressure on the garbage collector and adds up to measurable
jank. Maintain your row’s base appearance via className and CSS, and merge
with the style prop only for the unavoidable positioning
values react-window provides.
∞ React Infinite Scroll with react-window
Virtualization and React infinite scroll are
natural companions. Virtualization handles the rendering side — you never
have too many DOM nodes. Infinite scroll handles the data side — you never
load too much data upfront. Together, they let you present arbitrarily
large datasets with buttery performance and minimal memory footprint.
The idiomatic integration point is the onItemsRendered
callback. react-window calls it whenever the set of visible items changes,
providing visibleStartIndex, visibleStopIndex,
overscanStartIndex, and overscanStopIndex. When
visibleStopIndex approaches itemCount - 1, it’s
time to fetch the next page. However, building this logic robustly from
scratch is non-trivial (loading states, deduplication, error handling).
The react-window-infinite-loader library does exactly this
and integrates with react-window’s API without friction.
Infinite scroll with react-window-infinite-loader
import InfiniteLoader from 'react-window-infinite-loader'; import { FixedSizeList } from 'react-window'; function InfiniteList({ hasNextPage, isLoading, items, loadMore }) { // Total item count includes a placeholder for the loading row const itemCount = hasNextPage ? items.length + 1 : items.length; const isItemLoaded = (index) => !hasNextPage || index < items.length; const Item = ({ index, style }) => { if (!isItemLoaded(index)) { returnstyle={style}>Loading…; } returnstyle={style}>{items[index].name}; }; return (isItemLoaded={isItemLoaded} itemCount={itemCount} loadMoreItems={isLoading ? () => {} : loadMore} > {({ onItemsRendered, ref }) => ( ); }ref={ref} height={600} itemCount={itemCount} itemSize={52} width={'100%'} onItemsRendered={onItemsRendered} > {Item} )}
The InfiniteLoader wrapper injects an onItemsRendered
handler and a ref that it needs to track scroll position. You
pass both directly to your FixedSizeList. When the user
scrolls near the end, InfiniteLoader calls your
loadMoreItems function, which should fetch the next page and
update your state. The isItemLoaded function tells the loader
which indices are already in memory, preventing duplicate fetches. It’s a
clean separation of concerns: react-window owns rendering, InfiniteLoader
owns fetching logic, your state manager owns data.
🐛 Common Mistakes and How to Avoid Them
React window virtualization is conceptually simple, but its constraints
interact with React’s rendering model in ways that produce some genuinely
baffling bugs if you’re not expecting them. Here are the failure modes
most likely to cost you an afternoon.
-
Defining row renderers inside the parent component.
Every parent re-render creates a new function reference. React interprets
this as a new component type and fully unmounts/remounts every visible
row. Define row components at the module level or wrap in
React.memowith a stable reference. -
Forgetting to spread the style prop.
Thestylefrom react-window includesposition,
top,height, andwidth. Override
any of these and your rows will stack on top of each other or disappear.
Always dostyle={{ ...style, ...yourStyles }}. -
Setting height/width as percentages.
FixedSizeListrequires numeric pixel values for height and
width. Percentages will produce a zero-height list. Use
react-virtualized-auto-sizerto measure the container and
pass in pixel values. -
Not calling resetAfterIndex for VariableSizeList.
Any time item heights change at runtime, the internal size cache must be
invalidated. CalllistRef.current.resetAfterIndex(changedIndex)
or your layout will silently corrupt. -
Trying to use CSS overflow on the outer container.
react-window manages its own scroll container. Wrapping it in another
scrollable element creates a nested scroll context that confuses the
library’s position calculations. Keep the react-window container as the
scroll root.
challenges for screen readers and keyboard navigation. The DOM only
contains visible rows, so AT software can’t enumerate the full list.
Consider adding
role="listbox", managingaria-setsize and aria-posinset attributes,and testing with VoiceOver or NVDA if your list is a core UI element
rather than a data display.
One more pitfall worth mentioning: windowing and CSS Grid/Flexbox
on the container don’t mix well. react-window uses absolute
positioning internally. If the container has a flex or grid layout that
repositions children, the internal positioning calculations become
incorrect. Keep the direct parent of the list component as a plain block
or inline-block element, and apply your layout structure one level up.
❓ FAQ
Three questions that reliably surface in search results, forums, and
Stack Overflow threads about react-window — answered concisely.
What is the difference between react-window and react-virtualized?
Both libraries are by Brian Vaughn and implement the same windowing
concept. react-virtualized is the original, larger
library with ~30KB gzipped and a wider feature set: Masonry, Table,
Collection, CellMeasurer, and more. react-window
is a deliberate rewrite focused on the core use cases — lists and
grids — at roughly 6KB gzipped, with a cleaner and more consistent
API.
For new projects, start with react-window. Reach for
react-virtualized only if you specifically need features it provides
that react-window doesn’t (Masonry layout, for instance). The author
himself recommends react-window as the default choice.
How do I use react-window with variable item heights?
Use VariableSizeList instead of
FixedSizeList, and pass a function to
itemSize that returns the height in pixels for a given
index. If heights are known upfront (from your data), precompute them
into an array and index into it. If heights are determined by rendered
content, you’ll need to measure after mount using
ResizeObserver and call
listRef.current.resetAfterIndex() whenever a height
changes.
Also set estimatedItemSize to a reasonable average —
this improves scrollbar accuracy before all sizes are known.
Can react-window be combined with React infinite scroll?
Yes, and they complement each other well. The recommended approach
is to use the react-window-infinite-loader package,
which wraps your FixedSizeList or
VariableSizeList and manages the
“fetch more when approaching end” logic. It uses the
onItemsRendered callback to track scroll position and
calls your loadMoreItems function at the appropriate
time.
Alternatively, implement it manually: watch
visibleStopIndex from onItemsRendered and
trigger a fetch when it exceeds a threshold like
itemCount - 10. For production use, the
InfiniteLoader package handles edge cases (concurrent fetches,
error states, loading placeholders) that a manual implementation
easily misses.