4

Rendering long lists using virtualization with React

 1 year ago
source link: https://wanago.io/2022/06/27/long-lists-react-virtualization/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Rendering long lists using virtualization with React

React

June 27, 2022

When working on web applications, we often display lists of data. If the data is not particularly lengthy, we are probably okay with rendering all elements at once. However, as our data grows, we might notice performance issues. In this article, we create a simple React application that aims to render thousands of images using a virtualization technique.

Determining the issue

First, let’s create an application that fetches a list of photos and displays them. To do that, we will use JSONPlaceholder, which exposes an endpoint that returns five thousand entries.

For starters, we create a hook that fetches the data.

usePhotos.tsx
import { useEffect, useState } from 'react';
import Photo from './Photo';
function usePhotos() {
  const [photos, setPhotos] = useState<Photo[] | null>(null);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/photos?_limit=1000')
      .then((response) => response.json())
      .then((photosData) => {
        setPhotos(photosData);
  }, []);
  return {
    photos,
export default usePhotos;

Above, we use ?_limit=1000 to imit the number of photos to a thousand for now. Trying to render more is very likely to crash our browser.

Each one of our photos has a set of simple properties.

Photo.tsx
interface Photo {
  id: number;
  title: string;
  url: string;
  thumbnailUrl: string;
export default Photo;

Our list is straightforward and renders one component per photo.

PhotosList.tsx
import React from 'react';
import styles from './styles.module.scss';
import usePhotos from './usePhotos';
import PhotoCard from './PhotoCard/PhotoCard';
const PhotosList = () => {
  const { photos } = usePhotos();
  if (!photos) {
    return null;
  return (
    <div className={styles.wrapper}>
      {photos.map((photo) => (
        <PhotoCard key={photo.id} photo={photo} />
    </div>
export default PhotosList;

For each photo, we render the thumbnail and the title.

PhotoCard.tsx
import React, { FunctionComponent } from 'react';
import Photo from '../Photo';
import styles from './PhotoCard.module.scss';
interface Props {
  photo: Photo;
const PhotoCard: FunctionComponent<Props> = ({ photo }) => {
  return (
    <div className={styles.wrapper}>
      <a href={photo.url}>
        <img src={photo.thumbnailUrl} alt={photo.title} />
      </a>
      <p className={styles.title}>{photo.title}</p>
    </div>
export default PhotoCard;

Measuring the performance

While the above solution works, it is far from optimal. Rendering thousands of images at once causes the browser to make thousands of HTTP GET requests to fetch all of the data. Therefore, it is going to cause issues, especially on mobile.

Also, rendering thousands of elements in the DOM tree can slow down the initial rendering of our website and cause the scrolling to stutter.

dom.png

Let’s run a Lighthouse audit to measure the performance of our application.

lighthouse3.png

If you want to know more about Lighhouse, check out Improving our performance, accessibility, and SEO with Lighthouse audits

Unfortunately, the above score is not very good. So, let’s try improving it.

Introducing virtualization

If we render a large list, the user does not see all its contents at once and uses a scrollbar. When we implement virtualization, we don’t render the elements of the list that are not currently visible. By doing that, we make the DOM tree creation a lot faster. Besides that, the browser does not need to fetch all the images simultaneously.

To implement virtualization in this article, we use the react-window library.

npm install react-window @types/react-window

The react-window library is a lighter, more recent alternative for the react-virtualized package created by the same author.

Since every element of our list is the same size, we can use the FixedSizeList component provided by the react-window library.

PhotosList.tsx
import React from 'react';
import usePhotos from './usePhotos';
import PhotoCard from './PhotoCard/PhotoCard';
import { FixedSizeList } from 'react-window';
const PhotosList = () => {
  const { photos } = usePhotos();
  if (!photos) {
    return null;
  return (
    <FixedSizeList height={800} width={600} itemCount={photos.length} itemSize={155}>
      {({ index, style }) => {
        const photo = photos[index];
        return <PhotoCard key={photo.id} photo={photo} style={style} />;
    </FixedSizeList>
export default PhotosList;

FixedSizeList expects us to provide it with a component that renders an element of the list using the current index.

The component used for rendering a row might be a good canditate for memoization.

This component also gets the style prop used for positioning, so let’s modify our PhotoCard component to accommodate that.

PhotoCardtsx
import React, { CSSProperties, FunctionComponent } from 'react';
import Photo from '../Photo';
import styles from './PhotoCard.module.scss';
interface Props {
  photo: Photo;
  style?: CSSProperties;
const PhotoCard: FunctionComponent<Props> = ({ photo, style }) => {
  return (
    <div className={styles.wrapper} style={style}>
      <a href={photo.url}>
        <img src={photo.thumbnailUrl} alt={photo.title} />
      </a>
      <p className={styles.title}>{photo.title}</p>
    </div>
export default PhotoCard;

Thanks to the react-window library, we no longer render all elements at once.

dom2.png

The above, combined with the fact that the browser no longer fetches all of the images, immediately improves our performance significantly.

lighthouse2.png

Sizing the list automatically

So far, we’ve had to provide the width and height of the list explicitly. If we want the list to be sized automatically, we need the react-virtualized-auto-sizer library.

npm install react-virtualized-auto-sizer @types/react-virtualized-auto-sizer

The react-virtualized-auto-sizer library does not support React 18, currently.

To let the list grow to fill all available space, we need to use the AutoSizer component.

PhotosList.tsx
import React from 'react';
import usePhotos from './usePhotos';
import PhotoCard from './PhotoCard/PhotoCard';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
const PhotosList = () => {
  const { photos } = usePhotos();
  if (!photos) {
    return null;
  return (
    <AutoSizer>
      {({ height, width }) => (
        <FixedSizeList
          height={height}
          width={width}
          itemCount={photos.length}
          itemSize={155}
          {({ index, style }) => {
            const photo = photos[index];
            return <PhotoCard key={photo.id} photo={photo} style={style} />;
        </FixedSizeList>
    </AutoSizer>
export default PhotosList;

We can use AutoSizer to manage only width or height instead of both. To do that, we need to use the disableHeight or disableWidth  attributes.

A crucial thing to notice is that the AutoSizer expands to fill the parent, but it does not stretch it. In our case, to deal with it in the simplest way possible, we can enlarge the #root div.

#root {
  width: 100vw;
  height: 100vh;

Implementing pagination

So far, we’ve fetched all data in one big request. However, to improve the performance more and handle the data of any length, we can implement infinite scrolling. The idea is to fetch additional data every time the user scrolls to the end of the list.

To do that, we need the react-window-infinite-loader package.

npm install react-window-infinite-loader @types/react-window-infinite-loader

For the library to work, we need to use the InfiniteLoader component.

PhotosList.tsx
import React from 'react';
import usePhotos from './usePhotos';
import PhotoCard from './PhotoCard/PhotoCard';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import InfiniteLoader from 'react-window-infinite-loader';
const PhotosList = () => {
  const { photos, loadChunkOfData, checkIfPhotoLoaded } = usePhotos();
  return (
    <InfiniteLoader
      isItemLoaded={checkIfPhotoLoaded}
      loadMoreItems={loadChunkOfData}
      itemCount={Infinity}
      {({ onItemsRendered, ref }) => (
        <AutoSizer ref={ref}>
          {({ height, width }) => (
            <FixedSizeList
              height={height}
              width={width}
              itemCount={photos.length}
              itemSize={155}
              onItemsRendered={onItemsRendered}
              {({ index, style }) => {
                const photo = photos[index];
                return <PhotoCard key={photo.id} photo={photo} style={style} />;
            </FixedSizeList>
        </AutoSizer>
    </InfiniteLoader>
export default PhotosList;

The InfiniteLoader needs three properties:

  • isItemLoaded is a function that returns true if an element with a given index is loaded,
  • loadMoreItems is a function that fetches the data as soon as the user scrolls down,
  • itemCount is a number of rows in the list that can be an arbitrarily high number if the exact number of rows is unknown.

Let’s modify our usePhotos hook to accommodate for the properties the InfiniteLoader needs.

import { useCallback, useEffect, useRef, useState } from 'react';
import Photo from './Photo';
const elementsPerPage = 20;
function usePhotos() {
  const [isLoading, setIsLoading] = useState(false);
  const [hasNextPage, setHasNextPage] = useState(true);
  const [photos, setPhotos] = useState<Photo[]>([]);
  const didFetchInitialData = useRef(false);
  const loadChunkOfData = useCallback(async () => {
    if (isLoading) {
      return;
    setIsLoading(true);
    const pageNumber = photos.length / elementsPerPage;
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/photos?${new URLSearchParams({
        _start: String(elementsPerPage * pageNumber),
        _limit: String(elementsPerPage),
      })}`,
    const photosData = await response.json();
    if (photosData.length < elementsPerPage) {
      setHasNextPage(false);
    setPhotos([...photos, ...photosData]);
    setIsLoading(false);
  }, [isLoading, photos]);
  const checkIfPhotoLoaded = (index: number) => {
    return !hasNextPage || index < photos.length;
  useEffect(() => {
    if (!didFetchInitialData.current) {
      didFetchInitialData.current = true;
      loadChunkOfData();
  }, [loadChunkOfData]);
  return {
    photos,
    loadChunkOfData,
    checkIfPhotoLoaded,
export default usePhotos;

Thanks to the above changes, our application loads batches of elements as the user scrolls the list.

scrolling_optimized.gif

Summary

In this article, we’ve tackled the issue of rendering huge lists in React. To do that, we’ve used the react-window library. We’ve also learned how to integrate it with react-virtualized-auto-sizer and react-window-infinite-loader. Thanks to that, we’ve achieved an infinitely-loading list and improved the performance measured with the Lighthouse audit.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK