Improve Rendering Performance in a 1,000-Item React List

Improve Rendering Performance in a 1,000-Item React List

Imagine you are building some kind of application with React that requires you to show a long list of items. Say, 1,000 items. What are your options? Just render the whole thing at once?

Today, my goal is to show you the problem so that you understand what we are solving and then present a solution.

Let's get started!


Create the Project

At this point, all of you should know how to create a new React application from the command line using create-react-app. Just go to your terminal and run the following command to get going with a brand new React application: npx create-react-app long-list-app


Let's Render 1,000 Items

OK, let me first show you the problem. Have a look at the following component:

import React, {useEffect, useState} from 'react';

const generateNames = (count) => {
    const temp = [];
    for(let i=0;i<=count;i++) temp.push(`Test Name- ${i}`)
    return temp;
}

export const LongList = () => {
    const [names , setNames ] = useState([])

    useEffect(() => {
        setNames(generateNames(1000));
    },[])

    return <> {names.map(name => <ListItem name={name}/>)} </>
}

const ListItem = ({name}) => {
    console.log(`rendered ${name}`)
    return <div> Name is: {name} </div>
}

In this component, we are generating 1,000 names and rendering them inside a list. Now let's have a look in the browser:

1_zA89An2T8Jv6h-pR7PAs1w.png


Although our window is capable of showing six items, from the console, we can see that 1,000 items are being rendered in the background!

This inefficient rendering can make your application really slow.


Now Show Me the Code!

We will use an awesome library named react-virtualized. The concept is really simple and elegant:

Don't render stuff you don't see on the screen!

First, install the dependency:

yarn add react-virtualized

What this library does is export some wrapper components to wrap around your list. You provide the height, width, and a function to render, and the library handles the rest. Let's update our code to use the List component exported from the library:

import React, {useEffect, useState} from 'react';
import {List} from 'react-virtualized';

export const LongList = () => {

    // SAME AS BEFORE

    const renderRow = ({ index, key, style }) => {
        return <ListItem style={style} key={key} name={names[index]}/>
    }

    return <List
        width={800}
        height={900}
        rowHeight={30}
        rowRenderer={renderRow}
        rowCount={names.length}
    />
}

We have changed the rendering process with the List component.

Notice one thing here: We passed some extra props. width, height, rowHeight, and rowCount are self-explanatory, while rowRender is a render function for each row item. Now let's see the result:

1_BycFRJrNF4-bOzzSn8eq4w.png

Now we can see that there are 39 items on the screen and exactly 39 items are rendered now. No unnecessary rendering!


Variable-Sized Screen

You have likely noticed that we are giving the height and width of the container as a constant. But what if the screen of the users is of a different size. You just can't say, "But it works on my machine!"

react-virtualized has already solved this problem. It has another component named AutoSizer that helps to find the height and width of the container of the list dynamically. It follows a render-props pattern. Let's update our code like so:

import React, {useEffect, useState} from 'react';
import {List , AutoSizer} from 'react-virtualized';

export const LongList = () => {

    // SAME AS BEFORE

    return <div style={{height:"100vh"}}>
        <AutoSizer>
            {({ width, height }) => {
                return <List
                    width={width}
                    height={height}
                    rowHeight={30}
                    rowRenderer={renderRow}
                    rowCount={names.length}
                    overscanRowCount={3} />
            }}
        </AutoSizer>
    </div>
}

Now the height and width of the screen are automatically updated based on the user's screen size.

Variable-Sized Row

So we solved the height and width issue of the container. But what about the individual row items? What if their size varies based on the content?

react-virtualized has a solution for that too. You need to use another component named CellMeasurer for that. We will use another component named CellMeasurerCache for caching the size of the component.

Now look at the modified rendering process:

import React, {useEffect, useState} from 'react';
import {List, AutoSizer, CellMeasurerCache, CellMeasurer} from 'react-virtualized';

export const LongList = () => {

    // SAME AS BEFORE

    const cache = new CellMeasurerCache({
        defaultWidth: 500,
        defaultHeight: 900
    });

    const renderRow = ({ index, key, style , parent }) => {
        return <CellMeasurer
            key={key}
            cache={cache}
            parent={parent}
            columnIndex={0}
            rowIndex={index}>

            <div style={style}>
              Name is: {names[index]}
            </div>

        </CellMeasurer>
    }

    return <div className={'list'}>
        <AutoSizer>
            {({ width, height }) => {
                return <List
                    width={width}
                    height={200}
                    rowRenderer={renderRow}
                    rowCount={names.length}
                    rowHeight={cache.rowHeight}
                    deferredMeasurementCache={cache}
                />
            }}
        </AutoSizer>
    </div>

}

Conclusion

So there you have it. I just scratched the surface of what's possible with react-virtualized and how it can help us to significantly improve low-performing React applications. Have a great day!