React list virtualization: fast rendering for large lists





React list virtualization: fast rendering for large lists





React list virtualization: fast rendering for large lists

Quick practical guide — installation, examples, variable heights, infinite scroll, and performance optimization.

Rendering thousands of items in a React list without freezing the UI is a small art and a large engineering problem. This guide covers the why, the how, and the which — with clear installation steps, an example, and advanced tips to handle variable-height rows and infinite scrolling. Expect code, nuance, and a tiny bit of sarcasm where the DOM deserves it.

Why virtualize a React list (and when not to)

When you render a large array of items directly (map → JSX), the browser ends up with a huge DOM. More DOM nodes means more layout costs, more paint, more GC pressure, and a slower user experience. Virtualization — sometimes called windowing — keeps only the nodes that are visible (plus a small buffer) mounted in the DOM. The result: fewer nodes, faster paint, and scroll that’s smooth rather than juddery.

Not every list needs virtualization. If you have a dozen or a couple hundred small items, the overhead of a virtualization library may not be worth the complexity. For small lists, prefer readability and simple React.: optimize later if metrics demand it. Also avoid virtualization when SEO requires server-side rendering of every item or when every item needs a persistent DOM state that cannot be reconstructed cheaply.

Deciding factors include list length, item complexity, and scroll performance budgets on target devices. If your users are on low-power devices or you expect lists in the hundreds or thousands, start with virtualization early. If not, premature optimization is still premature — but now at least you know how to fix it.

Key concepts and libraries

Before coding, learn a few terms: windowing/virtualization, overscan (buffer items outside the viewport), virtualization container (the scrolling element), and measurement (fixed vs variable height). Keep these terms in mind as they determine which library fits your case.

Primary keywords: react-list, React list virtualization, React virtualized list, react-window, react-virtualized.
Secondary / LSI: list windowing, overscan, variable heights, CellMeasurer, react-virtuoso, infinite scroll, lazy loading, virtualization hooks.

Popular libraries: react-window (small, fast, best for fixed sizes), react-virtualized (feature-rich, supports measurement utilities), react-virtuoso (excellent variable-height support and convenient APIs), and newer hooks-based approaches built on top of ResizeObserver. Links: react-window (https://github.com/bvaughn/react-window), react-virtualized (https://github.com/bvaughn/react-virtualized), and a deeper advanced write-up on virtualizing lists: the Dev.to article on advanced virtualization.

Installation and getting started

Install the library that matches your needs. For a minimal, performant setup with mostly fixed-size rows, use react-window:

npm install react-window
# or
yarn add react-window

For variable-height rows or advanced features, prefer react-virtuoso or react-virtualized. Example installs:

npm install react-virtuoso
npm install react-virtualized

Once installed, the core pattern is the same: provide the list of items, tell the virtualizer the estimated or fixed item size, and render a row renderer that receives index and style. The style prop contains absolute positioning—don’t throw it away. It’s the thing that keeps your DOM in the right place without layout thrash.

Minimal example: react-window (fixed heights)

react-window is intentionally small. Here’s a canonical example using FixedSizeList. It’s short, performant, and perfect when each row height is known or consistent.

import { FixedSizeList as List } from 'react-window';

function Row({ index, style, data }) {
  const item = data[index];
  return <div style={style}>{item.title}</div>;
}

<List
  height={600}
  itemCount={items.length}
  itemSize={50}
  width="100%"
  itemData={items}
/>

Note: itemSize is the fixed height (or width for horizontal lists). The library uses this to compute the total scrollable size without measuring each item. If heights vary slightly but are similar, an estimated size still often works with acceptable experience, provided overscan is tuned.

For accessibility, ensure items remain focusable and that keyboard navigation logic is preserved; virtualization changes DOM presence, so manage focus carefully (save index on blur, restore after re-render).

Handling variable heights and complex UIs

Variable row heights are the real challenge. Naively rendering variable heights breaks assumptions used by fixed-size virtualizers. Solutions: measure heights, cache them, and let the virtualizer consult that cache. react-virtualized offers CellMeasurer to do this; react-virtuoso uses measurement under the hood for you; another path is to use ResizeObserver to notify the virtualizer of height changes.

Key steps for variable heights: (1) render rows and measure the DOM node height, (2) store that height in a cache keyed by item id or index, (3) let the virtualizer use the cached heights to calculate offsets, and (4) re-measure on content updates. This approach maintains scroll position and avoids expensive reflows because measurements are batched and cached.

Pro tip: for lists with many dynamic items (images, collapsed sections), use placeholders or estimated heights to avoid layout jank during initial measurements. Then replace estimates with real measurements progressively. Libraries like react-virtuoso reduce your work here — they handle many of these details internally and offer better out-of-the-box behavior for mixed-height content.

Infinite scroll and data fetching

Combining virtualization and infinite scroll is common: virtualization handles DOM/windowing, and infinite scroll handles data pagination. Keep responsibilities separated. Use the virtualization container’s scroll sentinel or an IntersectionObserver target near the end of the rendered window to trigger fetches for more items.

Important patterns: append new items to the data array (do not replace it), keep item keys stable (unique ids), and avoid remounting the entire list when new data arrives. If your virtualization calculates total size from item counts, ensure its internal size is updated when more items are appended. Also watch out for scroll jumps — when you insert items above the viewport, measure and adjust scroll offset to preserve user position.

For fetch triggers, prefer IntersectionObserver for reliability and less CPU than scroll events. Debounce or throttle network requests and show a small loading placeholder in the list. If you use server-side cursors, keep next-cursor logic in a small hook and feed results to the list component.

Performance optimization checklist

Use memoization for row renderers (React.memo) to avoid costly re-renders. Keep row components shallow and avoid inline functions that change each render. When using virtualization, the library already reduces DOM nodes, but re-renders of the visible window still matter.

Tune overscan: too small, and you risk janky scroll; too large, and you’re rendering extra DOM. Typical overscan values are one to three viewport-lengths worth of items, but tune based on device profiles and benchmarks. Use CPU throttling and simulate low-end devices during testing.

Measure, measure, measure. Use DevTools performance tab and Lighthouse, and test on actual low-end devices. Common hotspots: expensive render logic in rows, large images without lazy loading, and heavy CSS that forces repaint. Offload complex work (formatting, non-UI computation) to web workers where possible.

Advanced: preserving scroll, sticky headers, and animations

Preserving scroll position when items change is a practical problem. When inserting above the viewport, compute the added height and adjust scrollTop accordingly. Some libraries expose methods to scrollToPosition or to recompute sizes; others expect you to call recomputeRowHeights. Follow the library API to keep user context stable.

Sticky headers are more complex with virtualization because headers may need to be rendered outside the virtualized window to remain sticky. One approach is to render the sticky header in a separate layer (position: sticky) and compute its content based on the current first visible index. Some libraries include support for group headers or provide utilities to help.

Animations in virtualized lists should be used sparingly. Enter/exit transitions need nodes to exist long enough for animation, which conflicts with the virtualizer’s aggressive unmounting. Use CSS transitions on opacity when items are scrolled into view (and accept limited animation scope), or implement a small delay before unmounting to allow exit transitions — but test performance impact carefully.

Further reading and authoritative resources

If you want deep dives, practical examples, and trade-offs, read the original libraries’ docs and community articles. The official React docs explain rendering and reconciliation patterns; the GitHub READMEs for react-window and react-virtualized contain usage and migration notes; and an advanced article on list virtualization is available on Dev.to (Advanced list virtualization with react-list).

Also consider react-virtuoso for a modern, batteries-included approach to variable heights and grouping. When in doubt, prototype two approaches (fixed vs measured) and measure FPS/CPU to make a data-driven choice.

Finally, remember: virtualization fixes rendering bottlenecks but does not magically optimize slow business logic inside your row components. Profile both rendering and JavaScript execution to get a complete picture.

  • Quick checklist: choose library → install → set item size/measurement → render row → tune overscan → test on low-end devices.

FAQ

How do I virtualize a React list with variable row heights?

Use a library that supports measurement (react-virtualized with CellMeasurer or react-virtuoso), measure item heights (or use ResizeObserver), cache measurements, and let the virtualizer compute offsets from that cache. If possible, use estimated heights to avoid initial jank and re-measure progressively.

Can I combine infinite scroll with list virtualization?

Yes. Let the virtualization library manage DOM windowing and wire infinite-scroll fetching to the virtualization container’s scroll or an IntersectionObserver sentinel. Append data (don’t replace) and maintain stable keys to avoid remounts and scroll jumps.

Which library should I choose: react-window or react-virtualized?

Pick react-window for small, fast, fixed-size lists. Choose react-virtualized or react-virtuoso when you need advanced features like variable heights, complex grids, or measurement tools. Consider react-virtuoso for modern convenience with variable content.

Semantic core (clusters)

Main cluster (primary): react-list, React list virtualization, React virtualized list, react-list example, react-list tutorial, react-list getting started, react-list installation, react-list setup, React list component.

Performance & features: React performance optimization, react-list variable height, React scroll performance, React large list rendering, React infinite scroll, react-list advanced, React virtualized list, react-window, react-virtualized, react-virtuoso.

Related LSI and intent queries: list windowing, overscan, variable row heights, CellMeasurer, virtualization hooks, infinite scroll implementation, lazy loading list items, memoize row, scrollToItem, preserve scroll position, virtualization vs pagination, react-list example code, react-list installation npm.

Clusters by intent:

  • Informational: “how to virtualize a list in react”, “why use virtualization”, “react-window vs react-virtualized”.
  • Transactional/Navigational: “react-list installation”, “react-list setup”, “react-window npm”.
  • Commercial/Comparative: “react-window vs react-virtuoso”, “best library for variable height lists”.