Simohamed Marhraoui Vue and React developer | Linux enthusiast | Interested in FOSS

How to virtualize large lists using react-window

5 min read 1520

How To Virtualize Large Lists Using React Window

In this modern area of JavaScript-powered web pages, the DOM can be an expensive abstraction. Without the right tools to enhance performance, a single prop change in your React app can cause elements to re-render unnecessarily.

But even without the involvement of JavaScript, having a large DOM tree can slow down your pages and tank your Core Web Vitals, putting a burden on your network requests, runtime, and memory performances.

Standards for DOM size

DOM Dashboard With 76 Performance Rating

It’s important to keep in mind that although browsers can handle larger DOM trees, it’s advised to limit the total DOM nodes count to 1500, the DOM depth to 32, and the DOM node count for a single parent element to 60.

We can end up with an excessive DOM size by either sending a sizable HTML file over the wire or by generating elements at runtime until we exceed the performance budgets.

Using as-is, infinite scrolling, and pagination as alternatives to virtualization

When displaying a large set of data, there are many ways we can implement the visualization. The most notable ways to render the data set are via as-is, pagination, or infinite scrolling.

We can visualize these three options as such:

Single Column List Compared To Never Ending Column List Compared To New Page List

When we have continuous content, such as multiple paragraphs, on our page, we would use the “as-is” strategy to render our content. To optimize our page performance, we recourse to the CSS content-visibility property. See this blog post for more details.

However, using content-visibility would only help on the initial render. When we scroll down the page to the areas that the browser skipped rendering, we will end up with a slow-moving page again.

We made a custom demo for .
No really. Click here to check it out.

The same is true for infinite scrolling. The difference is that we only request content as it’s needed. However, we will eventually experience the same sluggish performance issues.

Pagination, on the other hand, is the most performant way to render. It displays only the necessary content on the initial render, it requests content as needed, and the DOM never bloats with needless content.

But, pagination is a pattern that isn’t suitable for displaying every large data set on a webpage. Instead, we can use virtualization.

What is virtualization?

Virtualization is a rendering concept that focuses on tracking the user’s position and only committing what is visually relevant to the DOM in any given scroll position. Essentially, it provides us with all the benefits of pagination along with the UX of infinite scrolling.

Rendering And Removing Column List

To virtualize a list, we pre-calculate the total height of our list using the dimensions of the given list items and multiplying it by the count of our list items.

Then, we position the items to create a list that the user can scroll through. Positioning our elements correctly is key to the efficiency of virtualization because individual items can be added or removed without affecting other items or causing them to reflow (i.e., the process of re-calculating an element’s position on the page).

However, there’s another way to render data.

How to virtualize a large list using react-window

To implement virtualization, we will use react-windo, which is a rewrite of react-virtualized. You can read a comparison between the two libraries here.

To install react-window, run the following:

$ yarn add react-window # the library
$ yarn add -D @types/react-window # auto-completion

react-window will be installed as a dependency, while the types for it will be installed as a devDependency even if we’re not using TypeScript. We will also need faker.js to generate our large data set.

$ yarn add faker

In our App.js, we will import faker as well as useState, and initialize our data state with faker’s address.city function. In our code, it will create an array with a length of 10000.

import React, { useState } from "react";
import * as faker from "faker";

const App = () => {
  const [data, setData] = useState(() =>
    Array.from({ length: 10000 }, faker.address.city)
  );

  return (
    <main>
      <ul style={{ width: "400px", height: "700px", overflowY: "scroll" }}>
        {data.map((city, i) => (
          <li key={i + city}>{city}</li>
        ))}
      </ul>
    </main>
  );
};

Next, we lazily initialize our state using a function to optimize for performance. Then, we make our list scrollable by giving it a width and a height and setting overflowY to scroll.

To compare the performance with and without virtualization, we will add a reverse button that reverses our data array.

const App = () => {
  const [data, setData] = useState(() =>
    Array.from({ length: 10000 }, faker.address.city)
  );

  const reverse = () => {
    setData((data) => data.slice().reverse());
  };

  return (
    <main>
      <button onClick={reverse}>Reverse</button>
      <ul style={{ width: "400px", height: "700px", overflowY: "scroll" }}>
        {data.map((city, i) => (
          <li style={{ height: "20px" }} key={i + city}>{city}</li>
        ))}
      </ul>
    </main>
  );
};

See the Pen
Non-virtualized list in React
by Simohamed (@smhmd)
on CodePen.

Now, try the reverse button and notice how latent the update is.

To virtualize this list, we will be using react-window’s FixedSizeList.

import { FixedSizeList as List } from "react-window";

const App = () => {
  const [data, setData] = useState(() =>
    Array.from({ length: 10000 }, faker.address.city)
  );
  const reverse = () => {
    setData((data) => data.slice().reverse());
  };

  return (
    <main>
      <button onClick={reverse}>Reverse</button>
      <List
        innerElementType="ul"
        itemCount={data.length}
        itemSize={20}
        height={700}
        width={400}
      >
        {({ index, style }) => {
          return (
            <li style={style}>
              {data[index]}
            </li>
          );
        }}
      </List>
    </main>
  );
};

We can use FixedSizeList in multiple ways. In this instance, we are creating an imaginary array with the same length of our data (through itemCount) and using it to index our data.

FixedSizeList’s children expose a render prop that has each index and the necessary styles (absolute positioning styles, etc.) passed into it.

We can also be explicit and pass our data and receive it in the render prop through itemData, like so:

<List
  itemData={data}
  innerElementType="ul"
  itemCount={data.length}
  itemSize={20}
  height={700}
  width={400}
>
  {({ data, index, style }) => {
    return <li style={style}>{data[index]}</li>;
  }}
</List>

Notice that our inline styles from earlier are now replaced with width and height props. overflowY is controlled by the layout prop, which defaults to vertical.

It’s important to pass the style render prop argument to the outermost element (the li, in our case). Without it, all elements will stack on top of one another and there will be nothing to scroll through.

The FixedSizeList elements render two wrapper elements that both default to divs and can be customized using innerElementType and outerElementType.

In our case, we set innerElementType to ul for accessibility reasons. However, only predefined props can be used. Adding props such as role or data-* will not have any effect.

By default, FixedSizeList will use the data indices as React keys. But because we are modifying our data array, we must use unique values for our keys. For that, FixedSizeList exposes the itemKey prop, which takes a function that should return either a string or a number. We will be using faker’s datatype.uuid function.

<List
  itemKey={faker.datatype.uuid}
  itemData={data}
  innerElementType="ul"
  itemCount={data.length}
  itemSize={20}
  height={700}
  width={400}
>
  {({ data, index, style }) => {
    return <li style={style}>{data[index]}</li>;
  }}
</List>

See the Pen
Virtualized list in React
by Simohamed (@smhmd)
on CodePen.

As I mentioned, we can instantaneously compare our virtualized list to the non-virtualized list using the reverse button. But the performance optimizations do not end there. If we have an expensive element that we render per each list item instead of our single li, react-window allows us to render a simple UI instead when scrolling.

To do this, we first need to enable the isScrolling boolean by passing useIsScrolling to our FixedSizeList.

<List
  useIsScrolling={true}
  itemCount={data.length}
  itemSize={20}
  height={700}
  width={400}
>
  {({ index, style, isScrolling }) =>
    isScrolling ? (
      <Skeleton style={style} />
    ) : (
      <ExpensiveItem index={index} style={style} />
    )
  }
</List>;

Here’s what that could look like:

Delayed Rendering And Removing Data After Scrolling

 

See the Pen
React Window’s isScrolling
by Simohamed (@smhmd)
on CodePen.

How to virtualize a grid with react-window

Now that we know how to virtualize a list, let’s learn to virtualize a grid. It’s a similar process, but the difference is that you have to add your data’s count and dimensions in both directions: vertically (columns) and horizontally (rows).

import { FixedSizeGrid as Grid } from "react-window";
import * as faker from "faker";

const COLUMNS = 18;
const ROWS = 30;

const data = Array.from({ length: ROWS }, () =>
  Array.from({ length: COLUMNS }, faker.internet.avatar)
);

function App() {
  return (
    <Grid
      columnCount={COLUMNS}
      rowCount={ROWS}
      columnWidth={50}
      rowHeight={50}
      height={500}
      width={600}
    >
      {({ rowIndex, columnIndex, style }) => {
        return <img src={data\[rowIndex\][columnIndex]} alt="" />;
      }}
    </Grid>
  );
}

See the Pen
React Window Grid
by Simohamed (@smhmd)
on CodePen.

Easy, right?

Conclusion

In this article, we covered the performance limits of the DOM as well as how to optimize a lean DOM using multiple rendering strategies. We also discussed how virtualization, through the use of react-window, can efficiently display large data sets to meet our performance targets.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Simohamed Marhraoui Vue and React developer | Linux enthusiast | Interested in FOSS

Leave a Reply