React Virtualization with react-window
Windowing, FixedSizeList, VariableSizeList, grid, and infinite scroll..
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
- ✓Node.js 16+, React 18+, npm or yarn, basic understanding of React hooks (useState, useRef, useCallback, useEffect), familiarity with JavaScript ES6 features (arrow functions, destructuring, spread operator).
React Virtualization with react-window: A core React concept for building modern user interfaces. It helps you structure your components efficiently and handle data flow predictably.
React is a JavaScript library for building user interfaces. This article covers virtualization — a key concept for building modern web applications.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
A comprehensive guide to react virtualization with react-window with production examples and best practices.
Why Virtualization Matters in Production
Rendering thousands of list items in the DOM is a common performance killer. Each DOM node consumes memory, and layout recalculations become expensive as the list grows. Virtualization solves this by only rendering the items visible in the viewport, plus a small overscan buffer. This reduces DOM nodes from thousands to tens, dramatically improving scroll performance, memory usage, and initial render time. In production, we've seen React apps crash on mobile devices when rendering 10,000+ items without virtualization. The fix: react-window, a lightweight library that handles the math and DOM management for you. It's not a silver bullet—if your items have complex rendering logic, you still need to memoize and optimize. But for long lists, it's the first tool you reach for.
FixedSizeList vs VariableSizeList: Choosing the Right Component
react-window provides two primary list components: FixedSizeList and VariableSizeList. FixedSizeList assumes every item has the same height, which allows O(1) calculations for scroll offset and visible range. It's the fastest option. VariableSizeList requires a callback to report each item's size, which adds overhead but handles dynamic content like chat messages or feeds. In production, always start with FixedSizeList. If your items vary in height, measure them once and cache the results. Avoid recalculating sizes on every render. For VariableSizeList, use the itemSize prop as a function that returns the height for a given index. react-window will call it for each visible item. To improve performance, memoize the size function or precompute an array of heights.
resetAfterIndex(index) to force recalculation. Forgetting this causes layout bugs.Overscan: The Hidden Performance Knob
Overscan controls how many extra items are rendered above and below the visible area. The default is 1 item per side, but in production you may need more to prevent blank flashes during fast scrolling. However, increasing overscan adds DOM nodes. On mobile, a high overscan can negate the benefits of virtualization. We recommend starting with overscanCount={2} and profiling. If you see white flashes, increase it. If frame drops occur, decrease it. react-window also supports overscanCount for both FixedSizeList and VariableSizeList. For grid components, use overscanColumnsCount and overscanRowsCount. In practice, a value of 2-5 works for most apps. For infinite scroll lists, consider a higher overscan to preload data.
ScrollTo and ScrollToItem: Programmatic Control
react-window exposes imperative methods on the list ref: scrollTo(scrollOffset) and scrollToItem(index, align). These are essential for features like 'scroll to top', 'scroll to new message', or restoring scroll position after data changes. scrollToItem accepts alignment: 'auto', 'smart', 'center', 'end', 'start'. In production, use scrollToItem with 'smart' to avoid jarring jumps. For lists that update frequently, debounce scroll calls to avoid layout thrashing. Also, when the list data changes (e.g., items added at the top), you may need to adjust scroll offset manually. react-window doesn't handle this automatically. Store the scroll offset in a ref and restore it after re-render.
scrollToItem with 'smart' alignment for smooth programmatic scrolling. Handle offset manually on data changes.Grids with FixedSizeGrid and VariableSizeGrid
react-window also provides grid components for two-dimensional virtualized layouts. FixedSizeGrid and VariableSizeGrid work similarly to their list counterparts but require columnCount, columnWidth, rowCount, and rowHeight. Use cases include spreadsheets, image galleries, and calendar views. In production, grids are trickier because both dimensions need virtualization. Always set overscanColumnsCount and overscanRowsCount to avoid blank cells. For VariableSizeGrid, the columnWidth and rowHeight props are functions. Cache these values to avoid recalculating on every render. Grids also support scrollToItem with align for both row and column indices.
Integrating with Infinite Scroll and Data Fetching
Virtualization pairs naturally with infinite scroll. Use a library like react-window-infinite-loader or implement your own. The pattern: render a fixed number of items, and when the user scrolls near the end, fetch more data and append to the list. react-window's onItemsRendered callback provides visibleStartIndex and visibleStopIndex. Use these to detect when the last visible item is near the end. In production, debounce the fetch to avoid multiple requests. Also, handle loading states by rendering a placeholder row at the bottom. When new data arrives, update the itemCount and optionally call scrollToItem to maintain position. Beware of scroll jumps when items are added above the viewport.
onItemsRendered to trigger data fetching near the end. Debounce and handle loading states.Memoization and Item Rendering Optimization
react-window re-renders items when the list scrolls. If your item component is expensive, wrap it in React.memo. The style prop passed to each item is a plain object; react-window reuses the same object reference for performance, but if you modify it, you break that optimization. Always apply the style to the outer element and avoid spreading it into child components. For items with complex content, consider using shouldComponentUpdate or PureComponent. In production, we've seen 50% render time reduction by memoizing item components. Also, avoid inline functions in the render prop—define the component outside or use useCallback.
itemData prop instead of closure to avoid re-creating the render function on every render.itemData to pass external data. Avoid inline functions in render props.Handling Dynamic Item Heights with Measurement
When items have unknown heights (e.g., user-generated content), you need to measure them after render. Use a library like react-window-size-listener or a custom ResizeObserver. The pattern: render items with a placeholder height, measure the actual height after mount, and update the size cache. Then call resetAfterIndex on the VariableSizeList ref. In production, batch updates to avoid excessive resets. Also, consider using a fixed height for most items and only variable for exceptions. For images, set a known aspect ratio or use a placeholder until loaded. Measuring on every resize can be expensive; debounce the observer.
Testing and Debugging Virtualized Lists
Testing virtualized lists requires special care because only a subset of items are rendered. Use react-window's FixedSizeList with a fixed itemCount in tests, and scroll to render specific items. For integration tests, use scrollToItem to bring an item into view before asserting. Avoid snapshot testing the entire list—it will be huge and change on every scroll. Instead, test individual item components. In production, add a debug mode that renders all items (disable virtualization) to compare behavior. Use React DevTools to inspect the list's state for visibleStopIndex and visibleStartIndex. For performance debugging, use the Profiler to measure render times.
virtualize={false} that renders a plain map instead of the list for easier debugging.Production Patterns: Error Boundaries and Fallbacks
Virtualized lists can fail in subtle ways: incorrect item counts, size mismatches, or scroll position corruption. Wrap your list in an error boundary to catch rendering errors. Also, provide a fallback UI when the list fails to load or has zero items. For empty states, render a message outside the list to avoid an empty virtualized container. In production, monitor for layout shifts using the Layout Instability API. If the list jumps unexpectedly, log the scroll offset and item count. Consider using a key prop on the list to force remount when data changes drastically. This is safer than trying to reset internal state.
Alternatives and When to Avoid react-window
react-window is not the only virtualization library. react-virtualized is older and heavier. react-virtuoso offers more features like sticky headers and grouping. TanStack Virtual (formerly react-virtual) is a headless solution that gives you full control. For simple lists, react-window is the best balance of size and performance. Avoid virtualization if your list is small (<100 items) or if items have highly variable heights that change frequently. Also, if you need complex animations during scroll, virtualization may fight with your animation library. In those cases, consider using CSS content-visibility: auto instead, which defers rendering without removing DOM nodes.
Performance Monitoring and Production Debugging
Once your virtualized list is in production, monitor its performance. Use the Performance API to measure scroll frame rates. Log visibleStartIndex and visibleStopIndex to detect if the list is rendering too many items. Watch for layout shifts using the Cumulative Layout Shift (CLS) metric. If CLS spikes, your item sizes may be inconsistent. Also, monitor memory usage: a growing DOM count indicates a leak or incorrect overscan. In React DevTools, check if item components are being unmounted and remounted unnecessarily. Use why-did-you-render to detect unnecessary re-renders. In production, we added a custom hook that reports render counts per item to our analytics.
TanStack Virtual as Modern Headless Standard
TanStack Virtual (formerly React Virtual) is a headless UI library for virtualizing large lists and grids. Unlike react-window, it provides hooks like useVirtualizer that give you full control over rendering without dictating markup. This makes it ideal for complex UIs where you need custom scroll containers, dynamic measurements, or integration with other libraries.
To get started, install @tanstack/react-virtual. The core hook useVirtualizer accepts options for the number of items, estimated size, and the scroll container ref. It returns a virtualizer object with getVirtualItems() and getTotalSize(). You map over virtual items to render only visible rows.
Example with fixed height items: ```jsx import { useVirtualizer } from '@tanstack/react-virtual'; import { useRef } from 'react';
function VirtualList({ items }) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, });
return ( <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}> {virtualizer.getVirtualItems().map((virtualItem) => ( <div key={virtualItem.key} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: virtualItem.size, transform: translateY(${virtualItem.start}px), }} > {items[virtualItem.index]} </div> ))} </div> </div> ); } ```
TanStack Virtual excels in scenarios requiring dynamic measurements, window scrolling, or reverse infinite scroll. It also supports sticky items, gap measurements, and smooth scrolling. Its headless nature means you can use any UI framework or plain CSS.
However, it has a steeper learning curve than react-window and requires more boilerplate for simple lists. For most production apps with complex requirements, TanStack Virtual is the modern standard.
CSS content-visibility: auto as Lightweight Alternative
For lists with fewer than 1000 items, CSS content-visibility: auto offers a simpler, zero-JavaScript alternative to full virtualization. This CSS property tells the browser to skip rendering off-screen elements until they are near the viewport, improving initial load and scroll performance.
To use it, apply content-visibility: auto to each list item. You should also set contain-intrinsic-size to reserve space for items before they are rendered, preventing layout shifts.
Example: ``css .list-item { content-visibility: auto; contain-intrinsic-size: 50px; / height of each item / } ``
This works well for static lists where items have predictable sizes. The browser handles visibility checks automatically. However, it's not a replacement for true virtualization when dealing with thousands of items because all DOM nodes are still present in memory. For lists under 1000 items, the performance gain is often sufficient.
Limitations: content-visibility doesn't work with dynamic heights unless you update contain-intrinsic-size dynamically. It also doesn't reduce memory usage for large datasets. For lists exceeding 1000 items, consider react-window or TanStack Virtual.
Browser support is excellent (Chrome, Firefox, Edge, Safari 15+). It's a great progressive enhancement: start with CSS, then upgrade to a library if performance issues arise.
Library Size Comparison Table
When choosing a virtualization library, bundle size matters, especially for performance-critical apps. Below is a comparison of popular React virtualization libraries:
| Library | Minified + Gzipped | Features | Best For |
|---|---|---|---|
| react-window | ~5KB | Fixed/variable size lists and grids, simple API | Simple lists, small bundles |
| TanStack Virtual | ~8KB | Headless, dynamic measurements, window scroll, sticky items | Complex UIs, custom scroll containers |
| react-virtuoso | ~15KB | Autoscroll, sticky headers, grouped lists, built-in infinite scroll | Feature-rich lists, chat apps |
| vlist | ~4KB | Minimal, fast, fixed size only | Ultra-lightweight fixed lists |
react-window is the smallest and most straightforward, ideal when you need a simple virtualized list with minimal overhead. TanStack Virtual offers more flexibility at a slightly larger size. react-virtuoso packs many built-in features but is heavier. vlist is the lightest but limited to fixed sizes.
For production, consider your trade-offs: bundle size vs. feature set. If you only need a basic list, react-window or vlist are excellent. If you anticipate complex requirements like dynamic heights or infinite scroll, TanStack Virtual provides a good balance. react-virtuoso is great for rapid development with many built-in features.
Always measure actual bundle impact using tools like webpack-bundle-analyzer or esbuild's metafile.
| File | Command / Code | Purpose |
|---|---|---|
| BasicFixedSizeList.jsx | const Row = ({ index, style }) => ( | Why Virtualization Matters in Production |
| VariableSizeListExample.jsx | const itemSizes = Array.from({ length: 1000 }, (_, i) => (i % 2 === 0 ? 50 : 100... | FixedSizeList vs VariableSizeList |
| OverscanExample.jsx | const Row = ({ index, style }) => ( | Overscan |
| ScrollToItemExample.jsx | const Example = () => { | ScrollTo and ScrollToItem |
| FixedSizeGridExample.jsx | const Cell = ({ columnIndex, rowIndex, style }) => ( | Grids with FixedSizeGrid and VariableSizeGrid |
| InfiniteScrollExample.jsx | const Example = () => { | Integrating with Infinite Scroll and Data Fetching |
| MemoizedRow.jsx | const Row = React.memo(({ index, style, data }) => { | Memoization and Item Rendering Optimization |
| DynamicHeightList.jsx | const DynamicHeightList = ({ items }) => { | Handling Dynamic Item Heights with Measurement |
| TestingList.test.jsx | const Row = ({ index, style }) => ( | Testing and Debugging Virtualized Lists |
| ErrorBoundaryList.jsx | class ListErrorBoundary extends React.Component { | Production Patterns |
| CSSContentVisibility.jsx | /* Alternative: CSS content-visibility */ | Alternatives and When to Avoid react-window |
| PerformanceMonitor.jsx | const useListPerformance = (listRef) => { | Performance Monitoring and Production Debugging |
| TanStackVirtualExample.jsx | function VirtualList({ items }) { | TanStack Virtual as Modern Headless Standard |
| styles.css | .list-item { | CSS content-visibility |
Key takeaways
itemData, and avoid inline functions. This can cut render times by 50%.Common mistakes to avoid
3 patternsNot understanding React component re-rendering
Ignoring the rules of hooks
Mutating state directly instead of using setState
Interview Questions on This Topic
What is the Virtual DOM and how does React use it?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
That's React. Mark it forged?
7 min read · try the examples if you haven't