Hands-on guide to building high-performance scroll lists in React — fixed and variable heights, infinite loading, and practical optimization patterns.
Rendering thousands of DOM nodes at once is a one-way ticket to janky scrolling and unhappy users. Virtualization (also called windowing) keeps the DOM small by only rendering the rows visible in the viewport plus a small buffer. In React apps this reduces reconciliation work, dramatically lowers memory usage, and keeps frame rates high.
The performance payoff is especially obvious on mobile and low-powered devices where rendering and layout are costly. Virtualization also reduces paint and reflow overhead: fewer nodes mean less CSS layout work and faster repaints. In practice, a virtualized list often converts a 100–300ms frame drop into a buttery 16ms interactive frame.
Virtualization is not magic — it requires careful handling of item heights, keys, and updates. But when implemented correctly, it lets you build features like infinite scroll, live updates, and rich item layouts without sacrificing smoothness. We’ll cover trade-offs and show practical examples using popular libraries and patterns.
The React ecosystem offers several solid virtualization libraries; pick one based on complexity, feature set, and bundle size. For simple fixed-size rows react-window is compact and fast. For variable heights or more advanced measurement features react-virtualized and react-list-style libraries can help. If you want an introductory walkthrough, see this react-list tutorial.
Installation is straightforward. Typical installs use npm or Yarn. Example:
npm install react-window react-virtualized react-list
# or
yarn add react-window react-virtualized react-list
The following libraries are commonly used (short summary):
Fixed-height lists are the simplest. With react-window you declare the item size and the list can compute offsets without measuring DOM nodes. This yields minimal runtime cost and excellent performance for uniform items:
import { FixedSizeList as List } from 'react-window';
{({ index, style }) => (
<div style={style} key={items[index].id}>{items[index].title}</div>
)}
Variable-height rows are trickier. Solutions include measuring each rendered item (using ResizeObserver or measurement helpers such as react-virtualized’s CellMeasurer) or using an estimated height plus dynamic adjustments. The trade-off: measurement adds CPU work, but it’s necessary when row heights vary significantly.
Infinite scroll combines virtualization with incremental data loading. Hook into the onScroll or onItemsRendered callbacks and fetch the next pages when the render window approaches the list end. Debounce network calls, use a loading sentinel row, and keep your state updates minimal to avoid re-rendering the whole list.
// simplified infinite-load pattern with react-window
const onItemsRendered = ({ visibleStopIndex }) => {
if (visibleStopIndex > items.length - 10 && !loading) {
loadMoreItems();
}
};
Overscan: render a small buffer of rows beyond the viewport to absorb rapid scrolls. Too small and you’ll flicker; too large and you lose memory savings. Start with an overscan of 2–5 rows for mobile, 5–10 for desktop complex items, and tune while profiling.
Memoization and keying: wrap row renderers with React.memo and avoid creating inline functions or objects inside render. Stable keys are critical — changing keys forces full DOM remounts and negates virtualization benefits. If item order can change, use a stable id (not the index).
SSR, accessibility, and focus management: server-rendered lists should render only a small initial chunk to avoid heavy HTML payloads. For keyboard and screen reader users, ensure off-screen items remain accessible where appropriate (aria-live, aria-setsize, and proper focus management). Virtualization can confuse assistive tech if not implemented carefully.
When debugging, use the browser Performance tab to record rendering and layout times, and the React DevTools profiler to identify re-render hotspots. For practical advanced guidance see this deep dive into advanced list virtualization with react-list.
Grouped keywords and LSI phrases used and targeted by this article. Use these naturally for internal linking, anchor text, and long-tail optimization.
{
"primary": [
"react-list",
"React list virtualization",
"React virtualized list",
"react-list tutorial",
"react-list installation"
],
"secondary": [
"React infinite scroll",
"react-list example",
"react-list setup",
"React large list rendering",
"React performance optimization"
],
"clarifying": [
"react-list variable height",
"React scroll performance",
"react-list advanced",
"React list component",
"react-list getting started",
"virtualized list vs windowing",
"overscan virtualized list",
"memoize row renderer",
"measure item height react"
],
"LSI_synonyms": [
"windowing",
"list windowing",
"virtual scrolling",
"infinite loading",
"lazy render rows",
"cell measurer",
"fixed-size list",
"variable-height list"
]
}
react-window is the lightest and fastest for fixed-size items; use it when rows are uniform and you want minimal bundle impact. react-virtualized is fuller-featured (measurement helpers, grids, etc.) and suits complex cases. react-list and similar libraries provide flexible patterns and can be simpler for particular use cases — see a practical react-list tutorial if you want an example that balances simplicity and features.
Options: measure items on render (ResizeObserver or library helpers), provide accurate estimates and correct them incrementally, or use a library that supports dynamic measurement (react-virtualized’s CellMeasurer or patterns in react-list). Measure only visible items and cache heights to reduce overhead.
Monitor the rendered window (onItemsRendered or onScroll). When the visible end approaches your data end (for example, within 10 items), trigger an async fetch. Show a loading sentinel row, and append data immutably to avoid remounting the entire list. Debounce or throttle to prevent duplicate loads.
Official resources and libraries: