react-infinite-scroll-component, react-virtuoso, and react-window are all React libraries designed to improve performance when rendering large lists or implementing infinite scroll behavior. They address the common problem of slow rendering, high memory usage, and poor user experience that occurs when trying to display thousands of items at once in the DOM. While they share this goal, their underlying approaches, APIs, and use cases differ significantly. react-window provides low-level virtualization primitives focused on performance and minimalism. react-virtuoso offers a higher-level, feature-rich virtualized list with built-in support for dynamic item sizes, headers, footers, and grouping. react-infinite-scroll-component is not a virtualization library but rather a wrapper that triggers load-more callbacks as the user scrolls near the bottom of a container, typically used alongside pagination strategies.
When building modern web apps, you’ll often face a choice: how do you efficiently render long lists without slowing down the browser? The three libraries — react-infinite-scroll-component, react-virtuoso, and react-window — each offer different strategies. One isn’t universally “better”; the right pick depends on your data, UX needs, and performance constraints.
react-infinite-scroll-component assumes you’re loading data in chunks (e.g., from a paginated API) and just need a way to detect when the user has scrolled near the bottom to fetch the next page. It does not virtualize — it renders every item you give it. So if you load 10 pages of 50 items each, all 500 DOM nodes stay in memory.
// react-infinite-scroll-component: Simple infinite scroll
import InfiniteScroll from 'react-infinite-scroll-component';
function MyList({ items, loadMore, hasMore }) {
return (
<InfiniteScroll
dataLength={items.length}
next={loadMore}
hasMore={hasMore}
loader={<div>Loading...</div>}
>
{items.map(item => <div key={item.id}>{item.name}</div>)}
</InfiniteScroll>
);
}
react-window takes the opposite approach: it only renders what’s visible (plus a small buffer), no matter how large your dataset. But it requires you to know or fix the height of each item ahead of time.
// react-window: Fixed-size list
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
function MyList({ itemCount }) {
return (
<List
height={600}
itemCount={itemCount}
itemSize={50}
width="100%"
>
{Row}
</List>
);
}
react-virtuoso sits in the middle: it virtualizes like react-window, but automatically measures item heights and supports dynamic content, headers, and more — all with less boilerplate.
// react-virtuoso: Auto-sizing list
import { Virtuoso } from 'react-virtuoso';
function MyList({ items }) {
return (
<Virtuoso
style={{ height: 600 }}
data={items}
itemContent={(index, item) => <div>{item.name}</div>}
/>
);
}
This is where the libraries diverge sharply.
react-window forces you to choose between two components:
FixedSizeList: all items same height (fastest)VariableSizeList: you must provide an itemSize function that returns the height for each indexIf your content height depends on text length or images, you’ll need to pre-calculate or cache sizes — which adds complexity.
// react-window: Variable size list
import { VariableSizeList as List } from 'react-window';
const getItemSize = (index) => {
// Must return exact pixel height for index
return Math.random() * 50 + 30; // Not realistic — you’d use real logic
};
const Row = ({ index, style }) => (
<div style={style}>Dynamic row {index}</div>
);
function MyList({ itemCount }) {
return (
<List
height={600}
itemCount={itemCount}
itemSize={getItemSize}
width="100%"
>
{Row}
</List>
);
}
react-virtuoso measures items automatically after they render. No need to guess heights — it works out of the box with variable content.
// react-virtuoso: Handles dynamic heights automatically
import { Virtuoso } from 'react-virtuoso';
function MyList({ messages }) {
return (
<Virtuoso
style={{ height: 600 }}
data={messages}
itemContent={(index, message) => (
<div>
<p>{message.text}</p>
<small>{message.timestamp}</small>
</div>
)}
/>
);
}
react-infinite-scroll-component doesn’t care about heights — it renders everything. So dynamic heights are trivial to implement, but performance suffers as the list grows.
What if you want both infinite loading and virtualization? Only react-virtuoso and react-window support this natively.
react-virtuoso makes it easy with the endReached prop:
// react-virtuoso: Infinite scroll + virtualization
import { Virtuoso } from 'react-virtuoso';
function InfiniteVirtuosoList({ items, loadMore, hasMore }) {
return (
<Virtuoso
style={{ height: 600 }}
data={items}
itemContent={(index, item) => <div>{item.name}</div>}
endReached={() => hasMore && loadMore()}
components={{
Footer: () => hasMore ? <div>Loading more...</div> : null
}}
/>
);
}
react-window requires manual scroll tracking using onItemsRendered and managing your own loading state:
// react-window: Manual infinite scroll
import { FixedSizeList as List } from 'react-window';
import { useEffect, useState } from 'react';
function InfiniteWindowList({ items, loadMore, hasMore, totalItemCount }) {
const [loading, setLoading] = useState(false);
const handleItemsRendered = ({ visibleStopIndex }) => {
if (!hasMore || loading) return;
// Trigger load when near the end
if (visibleStopIndex >= items.length - 5) {
setLoading(true);
loadMore().finally(() => setLoading(false));
}
};
return (
<List
height={600}
itemCount={totalItemCount}
itemSize={50}
width="100%"
onItemsRendered={handleItemsRendered}
>
{({ index, style }) => <div style={style}>{items[index]?.name || 'Loading...'}</div>}
</List>
);
}
react-infinite-scroll-component cannot be combined with virtualization — it’s either/or. If you try to wrap a virtualized list inside it, you’ll break scrolling detection.
Need sticky section headers? Or a “Load More” button at the bottom?
react-virtuoso supports this via the components prop:
// react-virtuoso: Custom header and footer
<Virtuoso
data={items}
itemContent={(index, item) => <Item {...item} />}
components={{
Header: () => <div className="sticky-header">Top</div>,
Footer: () => <button onClick={loadMore}>Load More</button>
}}
/>
It also has built-in support for grouped lists with sticky group headers.
react-window requires you to manually compose headers/footers outside the list or use react-window-infinite-loader (a separate package) for more complex scenarios.
react-infinite-scroll-component lets you put anything inside its children, so headers and footers are easy — but again, everything stays in the DOM.
react-window is the fastest because it avoids layout thrashing and uses pure components. But you trade convenience for control.react-virtuoso is slightly slower due to runtime measurements, but the difference is negligible for most apps, and it saves you from writing error-prone sizing logic.react-infinite-scroll-component has no performance optimizations — it’s just a scroll listener. Fine for short lists (<100 items), unusable for long ones.react-infinite-scroll-component if your list can grow beyond a few hundred items. It will cause jank, memory bloat, and slow re-renders.react-window if your items have unpredictable heights and you can’t pre-compute them reliably. You’ll spend more time debugging sizing than building features.react-virtuoso only if you’re in an extreme performance scenario (e.g., rendering 10k+ rows in a trading dashboard) and can guarantee fixed heights — then react-window might edge it out.| Feature | react-infinite-scroll-component | react-virtuoso | react-window |
|---|---|---|---|
| Virtualization | ❌ No | ✅ Yes | ✅ Yes |
| Dynamic Item Heights | ✅ (but renders all) | ✅ Automatic | ⚠️ Manual (itemSize fn) |
| Infinite Scroll Built-in | ✅ Yes | ✅ Via endReached | ❌ Manual implementation |
| Headers / Footers | ✅ Easy (but not virtualized) | ✅ Via components prop | ⚠️ Manual composition |
| Best For | Short paginated lists | Chat, feeds, dynamic content | Fixed-height grids, dashboards |
react-infinite-scroll-component is quick and simple.react-virtuoso is the smoothest experience.react-window gives you raw speed and control.Don’t mix infinite scroll with non-virtualized rendering — it’s a common anti-pattern that leads to degraded performance over time. Choose virtualization early if your dataset could grow large.
Choose react-infinite-scroll-component if you need a simple way to trigger data fetching when the user scrolls to the end of a list, and you're already handling rendering (e.g., with standard React components or another list library). It’s best suited for paginated APIs where you append new items to an existing list. Avoid it if you’re rendering thousands of items at once — it doesn’t virtualize content, so performance will degrade as the list grows.
Choose react-virtuoso when you need a full-featured virtualized list with minimal setup, especially if your items have dynamic or unknown heights, or if you require features like sticky headers, grouped items, or custom scroll containers. It handles complex scenarios out of the box and provides a clean, declarative API. It’s ideal for chat logs, activity feeds, or any list where content size varies.
Choose react-window when you need maximum performance and fine-grained control over virtualization, and your list items have fixed or predictable heights. It’s a lower-level tool that requires more manual configuration but gives you direct access to the rendering logic. Use it in performance-critical applications like dashboards or data grids where every millisecond counts and you can afford to manage item sizing yourself.
A component to make all your infinite scrolling woes go away with just 4.15 kB! Pull Down to Refresh feature
added. An infinite-scroll that actually works and super-simple to integrate!
npm install --save react-infinite-scroll-component
or
yarn add react-infinite-scroll-component
// in code ES6
import InfiniteScroll from 'react-infinite-scroll-component';
// or commonjs
var InfiniteScroll = require('react-infinite-scroll-component');
<InfiniteScroll
dataLength={items.length} //This is important field to render the next data
next={fetchData}
hasMore={true}
loader={<h4>Loading...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
// below props only if you need pull down functionality
refreshFunction={this.refresh}
pullDownToRefresh
pullDownToRefreshThreshold={50}
pullDownToRefreshContent={
<h3 style={{ textAlign: 'center' }}>↓ Pull down to refresh</h3>
}
releaseToRefreshContent={
<h3 style={{ textAlign: 'center' }}>↑ Release to refresh</h3>
}
>
{items}
</InfiniteScroll>
<div
id="scrollableDiv"
style={{
height: 300,
overflow: 'auto',
display: 'flex',
flexDirection: 'column-reverse',
}}
>
{/*Put the scroll bar always on the bottom*/}
<InfiniteScroll
dataLength={this.state.items.length}
next={this.fetchMoreData}
style={{ display: 'flex', flexDirection: 'column-reverse' }} //To put endMessage and loader to the top.
inverse={true} //
hasMore={true}
loader={<h4>Loading...</h4>}
scrollableTarget="scrollableDiv"
>
{this.state.items.map((_, index) => (
<div style={style} key={index}>
div - #{index}
</div>
))}
</InfiniteScroll>
</div>
The InfiniteScroll component can be used in three ways.
height prop if you want your scrollable content to have a specific height, providing scrollbars for scrolling your content and fetching more data.scrollableTarget prop to reference the DOM element and use it's scrollbars for fetching more data.height or scrollableTarget props, the scroll will happen at document.body like Facebook's timeline scroll.scrollableTarget (a parent element which is scrollable)
| name | type | description |
|---|---|---|
| next | function | a function which must be called after reaching the bottom. It must trigger some sort of action which fetches the next data. The data is passed as children to the InfiniteScroll component and the data should contain previous items too. e.g. Initial data = [1, 2, 3] and then next load of data should be [1, 2, 3, 4, 5, 6]. |
| hasMore | boolean | it tells the InfiniteScroll component on whether to call next function on reaching the bottom and shows an endMessage to the user |
| children | node (list) | the data items which you need to scroll. |
| dataLength | number | set the length of the data.This will unlock the subsequent calls to next. |
| loader | node | you can send a loader component to show while the component waits for the next load of data. e.g. <h3>Loading...</h3> or any fancy loader element |
| scrollThreshold | number | string | A threshold value defining when InfiniteScroll will call next. Default value is 0.8. It means the next will be called when user comes below 80% of the total height. If you pass threshold in pixels (scrollThreshold="200px"), next will be called once you scroll at least (100% - scrollThreshold) pixels down. |
| onScroll | function | a function that will listen to the scroll event on the scrolling container. Note that the scroll event is throttled, so you may not receive as many events as you would expect. |
| endMessage | node | this message is shown to the user when he has seen all the records which means he's at the bottom and hasMore is false |
| className | string | add any custom class you want |
| style | object | any style which you want to override |
| height | number | optional, give only if you want to have a fixed height scrolling content |
| scrollableTarget | node or string | optional, reference to a (parent) DOM element that is already providing overflow scrollbars to the InfiniteScroll component. You should provide the id of the DOM node preferably. |
| hasChildren | bool | children is by default assumed to be of type array and it's length is used to determine if loader needs to be shown or not, if your children is not an array, specify this prop to tell if your items are 0 or more. |
| pullDownToRefresh | bool | to enable Pull Down to Refresh feature |
| pullDownToRefreshContent | node | any JSX that you want to show the user, default={<h3>Pull down to refresh</h3>} |
| releaseToRefreshContent | node | any JSX that you want to show the user, default={<h3>Release to refresh</h3>} |
| pullDownToRefreshThreshold | number | minimum distance the user needs to pull down to trigger the refresh, default=100px , a lower value may be needed to trigger the refresh depending your users browser. |
| refreshFunction | function | this function will be called, it should return the fresh data that you want to show the user |
| initialScrollY | number | set a scroll y position for the component to render with. |
| inverse | bool | set infinite scroll on top |
Thanks goes to these wonderful people (emoji key):
Ankeet Maini 💬 📖 💻 👀 🚧 | Darsh Shah 🚇 |
This project follows the all-contributors specification. Contributions of any kind are welcome!