Virtualized Lists

Hello there👋

In modern web apps, performance is critical, especially when dealing with large data. As the UI becomes more data-intensive, developers face a challenge of figuring out how to efficiently render thousands or even millions of list items without compromising the user experience. A solution to that is Virtualized Lists. Virtualized lists lets us seamlessly display vast amounts of data while maintaining a smooth and responsive interface.
How do virtualized lists work?

They work by rendering only the visible portion of the list, i.e. only the items that can be contained in the device's viewport, improving performance by reducing the number of DOM elements that need to be processed (created and updated).

Key Concepts

Virtualized lists in React are a performance optimization technique for rendering large lists of data. Key concepts are:

  • Windowing or Virtualization: Only a 'window' or subset of the list (enough to fit in the viewport) is rendered based on the user's scroll position. As they scroll, new items are rendered, coming into view, while others outside the viewport are removed from the DOM.

  • Efficient Rendering: An effect of windowing is that our list is rendered efficiently. Only a small portion of the list is rendered, and then removed when the user scrolls past, therefore minimizing memory usage and avoiding the re-render of unnecessary items.

  • Smooth Scrolling: Virtualization reduces the workload for React's rendering engine, ensuring that scrolling remains smooth.

How it works

  1. If we have a list of 1000 items, but the user can only see 10 at a time, the virtualized list would render only 10. For this viewport driven rendering, the virtualized list calculates and uses the height of the items and the height of the container (viewport or parent element).

  2. The virtualized list then uses the scroll event to detect when a user scrolls, and based on the scroll position, calculates what items need to be rendered. As the user scrolls, the list calculates;

    • The position of the top of the viewport (i.e. the current scroll position)

    • Which item needs to be rendered based on that position

    • Which items can be removed from the DOM because they are no longer visible, therefore reducing memory usage, and making the UI more responsive.

  3. To improve the user experience and reduce a visible flickering or jumping of items, the virtualized list also employs a buffering technique. It creates a buffer for the visible items, which could be 2 more items, so that the items appear immediately as the user scrolls, making the transition smooth. So, for our example, if the user can see 10 items, the virtualized list might render 12 or 15 items, with the extra items existing just outside the viewport.

How to use Virtualized lists

There are already existing ways to utilize virtualized lists in React. Popular list virtualization libraries include:

  1. react-window: A lightweight and fast library for efficiently rendering large lists and tabular data. It supports both vertical and horizontal scrolling and offers basic list and grid components like FixedSizeList, VariableSizeList, and FixedSizeGrid.

  2. react-virtualized: A more feature-rich library that offers additional components like grids, tables, including components like VirtualizedList, VirtualizedGrid, and Masonry, offering customization options, as well as support for infinite scrolling.

An Example

In this example, we have a list of 40 robot pictures. We will use react-virtualized to implement virtualization on our list.

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

export default function ResizeListener() {
  // State to store the window dimensions
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  // useEffect to add event listener for window resize
  useEffect(() => {
    // event handler function to update windowSize state
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    // add event listener on component mount
    window.addEventListener("resize", handleResize);

    // cleanup function to remove event listener on component unmount
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []); // empty dependency array for running the effect only once after the initial render

  const rowRenderer = ({ key, index, style }) => (
    <div style={{display: "flex", gap: "5px", height: "180px"}} id={`robo-${key}`}>
      <img key={key} style={{height: "150px", width: "150px"}} src={`https://robohash.org/${key}`} />
      <p>Robot {index}</p>
    </div>
  );

  return (
    <div style={{height: "100%", display: "flex", justifyContent: "center"}}>
      <List
        width={500}
        height={windowSize.height}
        rowCount={40}
        rowHeight={180}
        rowRenderer={rowRenderer}
      />
    </div>
  );
}

This is how our virtualized list looks like:

You can see how elements are added and removed from the DOM as the user scrolls, helping to manage memory and make scrolling smoother.

Conclusion

In summary, virtualized lists only render what is visible, listen to scroll events to determine when new items need to be rendered, dynamically update the DOM ( removing or adding nodes as items leave and enter the viewport), and buffer items for smooth scrolling. They are a powerful way to optimize performance when dealing with large datasets and you should consider adding them to your application.

Happy Coding🎉