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.