Efficiently Rendering Lists in React

Using react-window to render virtualized lists in React

3 minute read()

This post is also available in different formats so you can read on the go or share it around!

React has great rendering performance yet when it comes to huge amounts of data, like any framework, it can become bogged down. When you’re faced with large lists of data rendering to the DOM that a user can scroll through, performance can take a hit. One solution is to use a virtualized list that renders only what the user sees on the screen. When the user scrolls down, the items are seamlessly replaced with new content.

I decided to try out react-window, a newer library from the author of the very popular react-virtualized. The key difference is this one is significantly smaller and lightweight. It’s not as fully featured, but it gets the job done on a tight budget.

A react-window Comparison

Let’s take a look at the finished result and then see how react-window was integrated into the project.Comparison on Chrome, with and without react-window

The video shows 1000 items in a column containing some dummy data generated using Faker.js. These cards intentionally use animations, drop shadows and transforms to simulate a more intensive rendering scenario. The frame-rate drops significantly and frequently below the target 60fps. Using react-window results in a much smoother experience with slight drops in frame-rate when scrolling rapidly.

Why do we need react-window?

DOM nodes need to be populated, animated, and rendered. The more nodes in the hierarchy, the more intensive it is to display. Even though modern browsers very efficient at this, performance can still suffer under certain circumstances. As with many performance related tips, optimization should be done when necessary not preemptively. If you notice some slow down with huge lists of data then perhaps react-window is for you.

A case for react-window

Let’s take a look at a React component that could do with some virtualization.

function CardList({ dataList }) {
  return (
    <div>
      {dataList.map(p => (
        <Card {...p} />
      ))}
    </div>
  )
}

function Card({ p, style = {} }) {
  return (
    <div style={style}>
      <div className="card">
        <img src={p.image} />
        <div className="details">
          <span className="title">{p.title}</span>
          <span className="subtitle">{p.subtitle}</span>
          <div className="company">{p.company}</div>
        </div>
        <p className="description">{p.description}</p>
        <div className="tags">
          {p.tags.map(t => (
            <span
              className="tag"
              style={{
                backgroundColor: t.color,
                boxShadow: `0px 3px rgba(0,0,0,0.5), 0px 3px ${t.color}`,
              }}
            >
              {t.name}
            </span>
          ))}
        </div>
      </div>
    </div>
  )
}

The core components without react-window We have a basic card which will show some user details and a profile picture. To make the cards less performant on purpose and simulate a more complex component, I have added a lot of styling in CSS that is more taxing for the browser. This includes animations, box-shadows and, transforms. A single card looks like this:

a profile picture with fake name, job title and company, a lorem ipsum description and some tags rendered in coloured badges

Implementing react-window

The implementation for this example is easy, we simply need to update the <CardList /> component to use react-window.

I will import react-window like so: import { FixedSizeList as List } from 'react-window';

function CardList({ dataList }) {
  return (
    <List
      height={Math.max(
        document.documentElement.clientHeight,
        window.innerHeight || 0
      )}
      itemCount={1000}
      itemSize={366}
      width="100%"
    >
      {({ index, style }) => <Card p={dataList[index]} style={style} />}
    </List>
  )
}
function Card({ p, style = {} }) {
  return (
    <div style={style}>
      <div className="card">
        <img src={p.image} />
        <div className="details">
          <span className="title">{p.title}</span>
          <span className="subtitle">{p.subtitle}</span>
          <div className="company">{p.company}</div>
        </div>
        <p className="description">{p.description}</p>
        <div className="tags">
          {p.tags.map(t => (
            <span
              className="tag"
              style={{
                backgroundColor: t.color,
                boxShadow: `0px 3px rgba(0,0,0,0.5), 0px 3px ${t.color}`,
              }}
            >
              {t.name}
            </span>
          ))}
        </div>
      </div>
    </div>
  )
}

The core components updated to use react-window I know the number of cards I want to display so I am using a FixedSizeList, this requires the itemCount and itemSize props to be set. The height and width props are set to take up the entire screen to match the original example.

The trick for rendering our react-window list is to provide the component with a render prop (in this case we provide the render props in the body of the component or the children prop). The index will be provided by react-window so it can render only what is on screen. For efficiency, it will also need to absolutely position our <Card /> components, this is so when a card goes off screen, it can be positioned back in the list with different data.

Comparison

Recording the performance over time in Firefox developer tools, we can see the stark difference between the original and our improved react-window version.

graph showing frame rate over time with lot's of peaks and troughs
Without react-window approx. 35fps

Without using react-window, the example frequently dropped below 30fps and showed relatively poor scrolling performance. Performance was impacted on first load due to the amount of images and thus network requests which had to be made.

graph showing frame rate over time as relatively steady and smooth

With react-window approx. 55fps

Using react-window, the performance rarely dipped below 60fps and didn’t suffer from any noticeable slow down when scrolling. Less images were requested on initial load which made startup much quicker too. Images are requested when they come into view and due to the small size of each image, they often loaded off-screen which results in minimal pop-in.

Final Thoughts

I recommend react-window where you need a lightweight virtualization library in react. It’s much more focused than react-virtualized and doesn’t have all of the extra features but it does a few specific jobs well and with a small code size budget. It can significantly improve scrolling performance with huge lists and/or complex list items. It is also easy to integrate with your existing React app and is easy to use.

Take a look at the documentation and demo to get started.


If you want to learn more about lazy loading in React 16 then check out this article I wrote about React Suspense.

React Suspense — Load the Essentials: How to leverage code-splitting easily in React 16


If you’d like to take a look at improving performance and saving user’s data, I wrote an article about Preact.

Save user’s data with a lighter alternative to React: A look at how Preact can cut the fat and still deliver

Seth Corker

A Fullstack Software Engineer working with React and Django. My main focus is JavaScript specialising in frontend UI with React. I like to explore different frameworks and technologies in my spare time. Learning languages (programming and real life) is a blast.