react-infinite-scroller, react-virtualized, and react-window are all React libraries designed to handle the rendering of large datasets, but they approach the problem with different strategies and levels of abstraction. react-infinite-scroller focuses on the user interaction pattern of infinite scrolling, loading more data as the user reaches the bottom of a list, often rendering all accumulated items in the DOM. react-virtualized is a comprehensive suite of high-performance components that use windowing (virtualization) to render only the visible rows, offering complex features like dynamic heights and grid layouts. react-window is the modern, lightweight successor to react-virtualized, created by the same author to provide a smaller bundle size and a simpler API for standard virtualization needs, though with fewer built-in complex features.
When building data-heavy dashboards, chat interfaces, or feeds, rendering thousands of DOM nodes at once will freeze your browser. The solution is to render only what the user can see. Three libraries have defined this space in the React ecosystem: react-infinite-scroller, react-virtualized, and react-window. While they all solve the "big list" problem, they do so with very different philosophies and trade-offs.
The most critical difference lies in how many DOM nodes each library creates. This decision dictates your application's memory usage and scroll performance.
react-infinite-scroller does not virtualize. It renders every single item you load into the actual DOM. It simply listens for scroll events to tell you when to fetch more data. If you load 5,000 items, you have 5,000 DOM nodes. This works fine for small lists but causes significant lag as the list grows.
// react-infinite-scroller: Renders ALL items in the DOM
import InfiniteScroll from 'react-infinite-scroller';
function Feed() {
const [items, setItems] = useState(initialItems);
const loadMore = () => {
// Fetch next page and append to existing list
setItems(prev => [...prev, ...newItems]);
};
return (
<InfiniteScroll
pageStart={0}
loadMore={loadMore}
hasMore={true}
>
{items.map(item => (
<div key={item.id} className="item">
{item.content}
</div>
))}
</InfiniteScroll>
);
}
react-virtualized uses windowing. It calculates which items are visible in the viewport and renders only those, plus a small buffer. It uses absolute positioning to place these items in a container that matches the full height of the dataset. The DOM stays light regardless of data size.
// react-virtualized: Renders ONLY visible items
import { List } from 'react-virtualized';
function Feed({ items }) {
const rowRenderer = ({ key, index, style }) => (
<div key={key} style={style} className="item">
{items[index].content}
</div>
);
return (
<List
width={600}
height={400}
rowCount={items.length}
rowHeight={50}
rowRenderer={rowRenderer}
/>
);
}
react-window also uses windowing but with a simpler, tree-shakable architecture. Instead of importing a massive List component, you import a lightweight FixedSizeList or VariableSizeList. It functions similarly to react-virtualized under the hood but strips away legacy baggage for a cleaner API.
// react-window: Renders ONLY visible items (Modern API)
import { FixedSizeList } from 'react-window';
function Feed({ items }) {
const Row = ({ index, style }) => (
<div style={style} className="item">
{items[index].content}
</div>
);
return (
<FixedSizeList
height={400}
width={600}
itemCount={items.length}
itemSize={50}
>
{Row}
</FixedSizeList>
);
}
One of the hardest problems in virtualization is handling rows that change height based on their content (e.g., a tweet with 1 line of text vs. one with an image and 10 lines).
react-infinite-scroller handles this naturally because it renders everything. The browser's standard layout engine manages the heights automatically. No special configuration is needed.
// react-infinite-scroller: Natural height handling
// No extra code needed; CSS determines height
<div className="item">
{longContent} {/* Height expands automatically */}
</div>
react-virtualized solves this with CellMeasurer. This component measures the content of a row after it renders and caches the height. The list then uses this cached height for positioning. It is powerful but requires boilerplate code and a cache reference.
// react-virtualized: Complex dynamic heights
import { CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 100,
});
const rowRenderer = ({ index, key, parent, style }) => (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<div style={style}>
{items[index].content}
</div>
</CellMeasurer>
);
react-window offers VariableSizeList. You must provide an itemSize function that returns the height for a given index. Crucially, you must manually tell the list when a row's height changes so it can recalculate positions. This gives you control but puts the burden of tracking state on you.
// react-window: Controlled dynamic heights
import { VariableSizeList } from 'react-window';
const listRef = useRef(null);
// Tell the list a specific row changed height
listRef.current.resetAfterIndex(index);
const Row = ({ index, style }) => (
<div style={style}>
{items[index].content}
</div>
);
<VariableSizeList
ref={listRef}
height={400}
width={600}
itemCount={items.length}
itemSize={index => getItemHeight(items[index])} // You define this logic
>
{Row}
</VariableSizeList>
Beyond basic lists, real-world apps often need grids, infinite loading triggers, or sticky headers.
react-infinite-scroller is specialized for the "infinite" pattern. It provides props like initialLoad, loadMore, and hasMore out of the box. It does not support grids or complex layouts natively; you would need to build those yourself or combine it with other tools.
// react-infinite-scroller: Built-in infinite logic
<InfiniteScroll
loadMore={loadMore}
hasMore={hasMore}
loader={<h4 key={0}>Loading...</h4>}
>
{/* Items */}
</InfiniteScroll>
react-virtualized is a "kitchen sink" library. It includes Grid, Table, AutoSizer (to fill parent width/height), and InfiniteLoader (to handle data fetching logic). The InfiniteLoader component wraps your list and manages the logic of requesting new pages, which is more robust than the simple callback in react-infinite-scroller.
// react-virtualized: Integrated InfiniteLoader
import { InfiniteLoader, List } from 'react-virtualized';
<InfiniteLoader
isRowLoaded={({ index }) => !!items[index]}
loadMoreRows={loadMoreRows}
rowCount={totalRowCount}
>
{({ onRowsRendered, registerChild }) => (
<List
ref={registerChild}
onRowsRendered={onRowsRendered}
// ... other props
/>
)}
</InfiniteLoader>
react-window is minimalist. It does not include InfiniteLoader or AutoSizer in the core package. Instead, the author publishes them as separate companion packages (react-window-infinite-loader, react-virtualized-auto-sizer). This keeps the core bundle tiny but requires you to install and wire up more dependencies for advanced features.
// react-window: Composable via separate packages
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import AutoSizer from 'react-virtualized-auto-sizer';
<AutoSizer>
{({ height, width }) => (
<InfiniteLoader
isItemLoaded={isItemLoaded}
loadMoreItems={loadMoreItems}
itemCount={itemCount}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={height}
width={width}
itemCount={itemCount}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
)}
</AutoSizer>
Choosing a library is also a bet on its future.
react-infinite-scroller sees infrequent updates. While it still works, the fundamental approach of rendering all DOM nodes is increasingly seen as an anti-pattern for large datasets in modern React development. It lacks support for newer React features like concurrent rendering optimizations.
react-virtualized is officially in maintenance mode. The creator, Brian Vaughn, has stated that no new features will be added. It relies on older React patterns (like componentDidMount and legacy context) which can cause warnings or issues in strict mode or future React versions. It remains stable but is effectively frozen.
react-window is the active, recommended path. It was built specifically to address the bundle size and complexity issues of react-virtualized. It uses modern React hooks and patterns, ensuring compatibility with React 18 and beyond. It receives regular updates and bug fixes.
| Feature | react-infinite-scroller | react-virtualized | react-window |
|---|---|---|---|
| Rendering | Full DOM (No Virtualization) | Windowed (Virtualized) | Windowed (Virtualized) |
| Performance | Poor with 1000+ items | Excellent | Excellent |
| Bundle Size | Small | Large (~100kb+) | Tiny (~15kb) |
| Dynamic Heights | Automatic (Native) | Complex (CellMeasurer) | Manual (resetAfterIndex) |
| Infinite Load | Built-in Props | InfiniteLoader Component | Separate Package |
| Status | Legacy | Maintenance Mode | Active / Recommended |
react-infinite-scroller is a legacy tool. Avoid it for new development unless you are strictly constrained by an existing codebase that cannot tolerate a refactor. The performance cost of rendering thousands of DOM nodes is too high for modern web standards.
react-virtualized is the heavy-duty tank. It solved hard problems years ago and is incredibly stable. However, its weight and lack of active development make it a poor choice for greenfield projects. Use it only if you need a specific, complex feature (like a very specific grid layout) that react-window cannot easily replicate.
react-window is the modern standard. It provides the performance benefits of virtualization with a fraction of the code. Its modular design encourages best practices, and its active maintenance ensures it will work smoothly with future React updates. For 95% of use cases — from simple dropdowns to massive data grids — react-window is the correct architectural choice.
Choose react-window for almost all new projects requiring high-performance lists or grids. It offers the best balance of small bundle size, modern API design, and speed. It is the ideal choice when you need to render thousands of items efficiently and your row heights are either fixed or can be estimated reliably without heavy measurement overhead.
Choose react-virtualized if your project requires complex, non-standard list behaviors that react-window does not support out of the box, such as intricate dynamic height calculations with CellMeasurer or specific sticky section headers in older React versions. Be aware that this library is in maintenance mode, has a larger bundle size, and uses a more verbose API compared to modern alternatives.
Choose react-infinite-scroller only if you are maintaining a legacy codebase that relies on its specific 'load more' trigger logic and you cannot afford a refactor. It is generally not recommended for new projects because it renders all items in the DOM, which leads to severe performance degradation and memory issues as the list grows beyond a few hundred items.
react-window is a component library that helps render large lists of data quickly and without the performance problems that often go along with rendering a lot of data. It's used in a lot of places, from React DevTools to the Replay browser.
If you like this project there are several ways to support it:
The following wonderful companies and individuals have sponsored react-window:
Begin by installing the library from NPM:
npm install react-window
TypeScript definitions are included within the published dist folder
Frequently asked questions can be found here.
Documentation for this project is available at react-window.vercel.app.
Renders data with many rows.
| Name | Description |
|---|---|
| rowComponent | React component responsible for rendering a row. This component will receive an ℹ️ The prop types for this component are exported as |
| rowCount | Number of items to be rendered in the list. |
| rowHeight | Row height; the following formats are supported:
ℹ️ Percentage heights can't be resolved during server rendering unless a ⚠️ Dynamic row heights are not as efficient as predetermined sizes. It's recommended to provide your own height values if they can be determined ahead of time. |
| rowProps | Additional props to be passed to the row-rendering component. List will automatically re-render rows when values in this object change. ⚠️ This object must not contain |
| Name | Description |
|---|---|
| className | CSS class name. |
| style | Optional CSS properties. The list of rows will fill the height defined by this style. |
| children | Additional content to be rendered within the list (above rows). This property can be used to render things like overlays or tooltips. |
| defaultHeight | Default height of list (in pixels) for initial render, before its actual height has been measured. ℹ️ Lists can't be measured during server rendering, so this value determines how many rows are server rendered.
It's recommended to set this to the expected height of the list (or to specify a pixel height via the |
| defaultScrollOffset | Initial scroll offset (in pixels) of the list. This value is only applied once, when the list first mounts with at least one row. ℹ️ Server-rendered HTML can't be pre-scrolled, so server rendering always starts at the first row; the default scroll offset is applied once the list hydrates. ⚠️ If the list can't be scrolled that far when the offset is applied (e.g. it is hidden with |
| listRef | Ref used to interact with this component's imperative API. This API has imperative methods for scrolling and a getter for the outermost DOM element. ℹ️ The |
| onResize | Callback notified when the List's outermost HTMLElement resizes. This may be used to (re)scroll a row into view. |
| onRowsRendered | Callback notified when the range of visible rows changes. |
| overscanCount | How many additional rows to render outside of the visible area. This can reduce visual flickering near the edges of a list when scrolling. |
| rowKey | Lists use the row index as a ℹ️ Custom keys can ensure better UX for sortable or filterable lists, particularly if your row components are stateful. Refer to the React documentation for more info. ⚠️ This prop cannot be auto-memoized because it is called during render.
It is important to always |
| scrollElement | Element that scrolls the list, for lists that scroll along with other content (e.g. the page):
The list renders at its full height and only renders the rows visible within the scroll element's viewport. Its position within the scroll element is measured whenever it renders, scrolls, or resizes, and whenever content before it is resized, inserted, or removed (e.g. an image loading above the list).
ℹ️ While the scroll element is ⚠️ The list itself doesn't scroll, so its |
| tagName | Can be used to override the root HTML element rendered by the List component. The default value is "div", meaning that List renders an HTMLDivElement as its root. ⚠️ In most use cases the default ARIA roles are sufficient and this prop is not needed. |
Renders data with many rows and columns.
ℹ️ Unlike List rows, Grid cell sizes must be known ahead of time.
Either static sizes or something that can be derived (from the data in CellProps) without rendering.
| Name | Description |
|---|---|
| cellComponent | React component responsible for rendering a cell. This component will receive ℹ️ The prop types for this component are exported as |
| cellProps | Additional props to be passed to the cell-rendering component. Grid will automatically re-render cells when values in this object change. ⚠️ This object must not contain |
| columnCount | Number of columns to be rendered in the grid. |
| columnWidth | Column width; the following formats are supported:
ℹ️ Percentage widths can't be resolved during server rendering unless a |
| rowCount | Number of rows to be rendered in the grid. |
| rowHeight | Row height; the following formats are supported:
ℹ️ Percentage heights can't be resolved during server rendering unless a |
| Name | Description |
|---|---|
| className | CSS class name. |
| dir | Indicates the directionality of grid cells. ℹ️ The directionality of an ancestor element can't be detected during server rendering; specify this prop explicitly for server-rendered right-to-left grids. ℹ️ See HTML |
| style | Optional CSS properties. The grid of cells will fill the height and width defined by this style. |
| children | Additional content to be rendered within the grid (above cells). This property can be used to render things like overlays or tooltips. |
| columnKey | Grids use the column index as a ℹ️ Custom keys can ensure better UX for sortable or filterable grids, particularly if your cell components are stateful. Refer to the React documentation for more info. ⚠️ This prop cannot be auto-memoized because it is called during render.
It is important to always |
| defaultHeight | Default height of grid (in pixels) for initial render, before its actual height has been measured. ℹ️ Grids can't be measured during server rendering, so this value determines how many rows are server rendered.
It's recommended to set this to the expected height of the grid (or to specify a pixel height via the |
| defaultScrollLeft | Initial horizontal scroll offset (in pixels) of the grid. This value is only applied once, when the grid first mounts with at least one column. ℹ️ Server-rendered HTML can't be pre-scrolled, so server rendering always starts at the first column; the default scroll offset is applied once the grid hydrates. ⚠️ If the grid can't be scrolled that far when the offset is applied (e.g. it is hidden with |
| defaultScrollTop | Initial vertical scroll offset (in pixels) of the grid. This value is only applied once, when the grid first mounts with at least one row. ℹ️ Server-rendered HTML can't be pre-scrolled, so server rendering always starts at the first row; the default scroll offset is applied once the grid hydrates. ⚠️ If the grid can't be scrolled that far when the offset is applied (e.g. it is hidden with |
| defaultWidth | Default width of grid (in pixels) for initial render, before its actual width has been measured. ℹ️ Grids can't be measured during server rendering, so this value determines how many columns are server rendered.
It's recommended to set this to the expected width of the grid (or to specify a pixel width via the |
| gridRef | Imperative Grid API. ℹ️ The |
| onCellsRendered | Callback notified when the range of rendered cells changes. |
| onResize | Callback notified when the Grid's outermost HTMLElement resizes. This may be used to (re)scroll a cell into view. |
| overscanCount | How many additional rows/columns to render outside of the visible area. This can reduce visual flickering near the edges of a grid when scrolling. |
| rowKey | Grids use the row index as a ℹ️ Custom keys can ensure better UX for sortable or filterable grids, particularly if your cell components are stateful. Refer to the React documentation for more info. ⚠️ This prop cannot be auto-memoized because it is called during render.
It is important to always |
| scrollElement | Element that scrolls the grid, for grids that scroll along with other content (e.g. the page):
The grid renders at its full height and only renders the rows (and columns) visible within the scroll element's viewport. Its position within the scroll element is measured whenever it renders, scrolls, or resizes, and whenever content before it is resized, inserted, or removed (e.g. an image loading above the grid).
ℹ️ While the scroll element is ⚠️ The grid's |
| tagName | Can be used to override the root HTML element rendered by the Grid component. The default value is "div", meaning that Grid renders an HTMLDivElement as its root. ⚠️ In most use cases the default ARIA roles are sufficient and this prop is not needed. |