Virtualize large lists with react-window
Super large tables and lists can slow down your site's performance signficantly. Virtualization can help!
react-window
is a library that allows large lists to be rendered efficiently.
Here's an example of a list that contains 1000 rows being rendered with react-window
. Try scrolling as fast you can.
Why is this useful? #
There may be times where you need to display a large table or list that contains many rows. Loading every single item on such a list can affect performance significantly.
List virtualization, or "windowing", is the concept of only rendering what is visible to the user. The number of elements that are rendered at first is a very small subset of the entire list and the "window" of visible content moves when the user continues to scroll. This improves both the rendering and scrolling performance of the list.
DOM nodes that exit the "window" are recycled, or immediately replaced with newer elements as the user scrolls down the list. This keeps the number of all rendered elements specific to the size of the window.
react-window #
react-window
is a small, third-party library that makes it easier to create virtualized lists in your application. It provides a number of base APIs that can be used for different types of lists and tables.
When to use fixed size lists #
Use the FixedSizeList
component if you have a long, one-dimensional list of equally sized items.
import React from 'react';
import { FixedSizeList } from 'react-window';
const items = [...] // some list of items
const Row = ({ index, style }) => (
<div style={style}>
{/* define the row component using items[index] */}
</div>
);
const ListComponent = () => (
<FixedSizeList
height={500}
width={500}
itemSize={120}
itemCount={items.length}
>
{Row}
</FixedSizeList>
);
export default ListComponent;
- The
FixedSizeList
component accepts aheight
,width
anditemSize
prop to control the size of the items within the list. - A function that renders the rows is passed as a child to
FixedSizeList
. Details about the particular item can be accessed with theindex
argument (items[index]
). - A
style
parameter is also passed in to the row rendering method that must be attached to the row element. List items are absolutely positioned with their height and width values assigned inline, and thestyle
parameter is responsible for this.
The Glitch example shown earlier in this article shows an example of a FixedSizeList
component.
When to use variable sized lists #
Use the VariableSizeList
component to render a list of items that have different sizes. This component works in the same way as a fixed size list, but instead expects a function for the itemSize
prop instead of a specific value.
import React from 'react';
import { VariableSizeList } from 'react-window';
const items = [...] // some list of items
const Row = ({ index, style }) => (
<div style={style}>
{/* define the row component using items[index] */}
</div>
);
const getItemSize = index => {
// return a size for items[index]
}
const ListComponent = () => (
<VariableSizeList
height={500}
width={500}
itemCount={items.length}
itemSize={getItemSize}
>
{Row}
</VariableSizeList>
);
export default ListComponent;
The following embed shows an example of this component.
The item size function passed to the itemSize
prop randomizes the row heights in this example. In a real application however, there should be actual logic defining the sizes of each item. Ideally, these sizes should be calculated based on data or obtained from an API.
Grids #
react-window
also provides support for virtualizing multi-dimensional lists, or grids. In this context, the "window" of visible content changes as the user scrolls horizontally and vertically.
Similarly, both FixedSizeGrid
and VariableSizeGrid
components can be used depending on whether the size of specific list items can vary.
- For
FixedSizeGrid
, the API is about the same but with the fact that heights, widths and item counts need to be represented for both columns and rows. - For
VariableSizeGrid
, both the column widths and row heights can be changed by passing in functions instead of values to their respective props.
Take a look at the documentation to see examples of virtualized grids.
Lazy loading on scroll #
Many websites improve performance by waiting to load and render items in a long list until the user has scrolled down. This technique, commonly referred to as "infinite loading", adds new DOM nodes into the list as the user scrolls past a certain threshold close to the end. Although this is better than loading all items on a list at once, it still ends up populating the DOM with thousands of row entries if the user has scrolled past that many. This can lead to an excessively large DOM size, which starts to impact performance by making style calculations and DOM mutations slower.
The following diagram might help summarize this:
The best approach to solve this problem is continue to use a library like react-window
to maintain a small "window" of elements on a page, but to also lazy load newer entries as the user scrolls down. A separate package, react-window-infinite-loader
, makes this possible with react-window
.
Consider the following piece of code which shows an example of state that is managed in a parent App
component.
import React, { Component } from 'react';
import ListComponent from './ListComponent';
class App extends Component {
constructor(props) {
super(props);
this.state = {
items: [], // instantiate initial list here
moreItemsLoading: false,
hasNextPage: true
};
this.loadMore = this.loadMore.bind(this);
}
loadMore() {
// method to fetch newer entries for the list
}
render() {
const { items, moreItemsLoading, hasNextPage } = this.state;
return (
<ListComponent
items={items}
moreItemsLoading={moreItemsLoading}
loadMore={this.loadMore}
hasNextPage={hasNextPage}
/>
);
}
}
export default App;
A loadMore
method is passed into a child ListComponent
that contains the infinite loader list. This is important because the infinite loader needs to fire a callback to load more items once the user has scrolled past a certain point.
Here's how the ListComponent
that renders the list can look like:
import React from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from "react-window-infinite-loader";
const ListComponent = ({ items, moreItemsLoading, loadMore, hasNextPage }) => {
const Row = ({ index, style }) => (
{/* define the row component using items[index] */}
);
const itemCount = hasNextPage ? items.length + 1 : items.length;
return (
<InfiniteLoader
isItemLoaded={index => index < items.length}
itemCount={itemCount}
loadMoreItems={loadMore}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={500}
width={500}
itemCount={itemCount}
itemSize={120}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
)
};
export default ListComponent;
In here, the FixedSizeList
component is wrapped within the InfiniteLoader
. The props assigned to the loader are:
isItemLoaded
: Method that checks whether a certain item has loadeditemCount
: Number of items on the list (or expected)loadMoreItems
: Callback that returns a promise that resolves to additional data for the list
A render prop is used to return a function that the list component uses in order to render. Both onItemsRendered
and ref
attributes are attributes that need to be passed in.
The following is an example of how infinite loading can work with a virtualized list.
Scrolling down the list might feel the same, but a request is now made to retrieve 10 users from a random user API every time you scroll close to the end of the list. This is all done while only rendering a single "window" of results at at a time.
By checking the index
of a certain item, a different loading state can be shown for an item depending on whether a request has been made for newer entries and the item is still loading.
For example:
const Row = ({ index, style }) => {
const itemLoading = index === items.length;
if (itemLoading) {
// return loading state
} else {
// return item
}
};
Overscanning #
Since items in a virtualized list only change when the user scrolls, blank space can briefly flash as newer entries are about to be displayed. You can try quickly scrolling any of the previous examples in this guide to notice this.
To improve the user experience of virtualized lists, react-window
allows you to overscan items with the overscanCount
property. This allows you to define how many items outside of the visible "window" to render at all times.
<FixedSizeList
//...
overscanCount={4}
>
{...}
</FixedSizeList>
overscanCount
works for both the FixedSizeList
and VariableSizeList
components and has a default value of 1. Depending on how large a list is as well as the size of each item, overscanning more than just one entry can help prevent a noticeable flash of empty space when the user scrolls. However, overscanning too many entries can affect performance negatively. The whole point of using a virtualized list is to minimize the number of entries to what the user can see at any given moment, so try to keep the number of overscanned items as low as possible.
For FixedSizeGrid
and VariableSizeGrid
, use the overscanColumnsCount
and overscanRowsCount
properties to control the number of columns and rows to overscan respectively.
Conclusion #
If you are unsure where to begin virtualizing lists and tables in your application, follow these steps:
- Measure rendering and scrolling performance. This article shows how the FPS meter in Chrome DevTools can be used to explore how efficiently items are rendered on a list.
- Include
react-window
for any long lists or grids that are affecting performance. - If there are certain features not supported in
react-window
, consider usingreact-virtualized
if you cannot add this functionality yourself. - Wrap your virtualized list with
react-window-infinite-loader
if you need to lazy load items as the user scrolls. - Use the
overscanCount
property for your lists and theoverscanColumnsCount
andoverscanRowsCount
properties for your grids to prevent a flash of empty content. Do not overscan too many entries as this will affect performance negatively.