Improve the performance of dropdown containing large data using Virtualization

While working on one of our client projects, we noticed the application becoming laggy, and at times even froze due to the large data set getting loaded in the dropdown.

In this blog, we will share how the problem was solved using ‘Virtualization’.

Let’s first go through the term ‘Virtualization’.

What is Virtualization or Windowing?

Windowing or List virtualization is a technique of only rendering the visible portion in the current “window” to the DOM.

The number of items that are rendered for the first time is the subset of a large list. As the user scrolls down the list, the newer elements replace the old ones. This keeps the number of all rendered elements specific to the size of the window.

Virtualization or Windowing by Brian Vaughn Virtualization or Windowing by Brian Vaughn

Now, let us consider a dropdown that has more than 1000+ options to be loaded.

Loading complete data at once

import React, { useEffect, useState } from 'react';
import Select from 'react-select';
import axios from "axios";

function App() {
  const [options, setOptions] = useState([]);
  const [selectedValue, setSelectedOption] = useState('');

  useEffect(async () => {
    return await loadOptions();
  }, [])

  const loadOptions = async () => {
    try {
      const { data: { data } }  = await axios.get(`https://api.instantwebtools.net/v1/passenger`);
      const dataOptions = data.map(({ _id, name }) => (
        {
          label: name,
          value: _id
        }
      ));
      setOptions(dataOptions);
    } catch (err) {
      console.log(err); // eslint-disable
    }
  };

  return (
    <div className="App">
      <div className="dropdown">
        <div className="label"><label>Passenger name</label></div>
        <Select
          placeholder="Select"
          options={options}
          value={selectedValue}
          onChange={(selected) => setSelectedOption(selected)}
          isClearable
        />
      </div>
    </div>
  );
}
export default App;

Loading 1000+ options the first time caused dropdown to become unresponsive.

If we look at the performance, the FPS( Frames per second ) is light red, which is not a good indication.

Let us see how virtualization can help to improve the performance of the dropdown.

Using lazy loading and pagination

To improve the performance of the dropdown we can use lazy loading or in other words ‘infinite loading’. Infinite loading adds new DOM nodes into the list as the user scrolls past a certain threshold close to the end.

We have used the below packages in our code -

  1. react-window-infinite-loader - Lazy loading
  2. react-window- Renders a part of a large data set
  3. react-virtualized-auto-sizer - Automatically adjusts the width and height of a component
  4. react-select

Here, we fetch 200 options per page.

  • hasNextPage is used to check if there are more pages to be loaded.

  • isNextPageLoading checks if the data is being loaded and accordingly shows a loader in the menu list.

  • loadNextPage is a callback to be invoked when more rows must be loaded. It should return a Promise that is resolved once all data has finished loading.

//App.jsx

import React, { useState } from 'react';
import SelectWrapper from './SelectWrapper';
import axios from "axios";

function App() {
  const [options, setOptions] = useState([]);
  const [pageNo, setPage] = useState(0);
  //Checks if there are more pages to be loaded
  const [hasNextPage, setHasNextPage] = useState(true);
  const [isNextPageLoading, setNextPageLoading] = useState(false);
  const [selectedValue, setSelectedOption] = useState('');

  const loadOptions = async (page) => {
    try {
      const params = {
        page,
        size: 200
      };
      setNextPageLoading(true);
      const { data: { data, totalPassengers } } = await axios.get(`https://api.instantwebtools.net/v1/passenger`, { params });
      const dataOptions = data.map(({ _id, name }) => (
        {
          label: name,
          value: _id
        }
      ));
      const itemsData = options.concat(dataOptions);
      setOptions(itemsData);
      setNextPageLoading(false);
      setHasNextPage(itemsData.length < totalPassengers);
      setPage(page);
    } catch (err) {
      console.log(err); // eslint-disable
    }
  };

  const loadNextPage = async () => {
    await loadOptions(pageNo + 1);
  };

  return (
    <div className="App">
      <div className="dropdown">
        <div className="label"><label>Passenger name</label></div>
        <SelectWrapper
          value={selectedValue}
          placeholder="Select"
          isClearable
          hasNextPage={hasNextPage}
          isNextPageLoading={isNextPageLoading}
          options={options}
          loadNextPage={loadNextPage}
          onChange={(selected) => setSelectedOption(selected)}
        />
      </div>
    </div>
  );
}

export default App;

FixedSizeList is wrapped in InfiniteLoader for lazy loading. The props assigned to the loader are:

  • isItemLoaded: Function responsible for tracking the loaded state of each item.

  • itemCount: Number of rows in the list

  • loadMoreItems: Callback to be invoked when more rows must be loaded.

//SelectWrapper.jsx

import React, { useEffect, useState } from 'react';
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import Select from 'react-select';
import AutoSizer from 'react-virtualized-auto-sizer';

const SelectWrapper = (props) => {
  const {
    hasNextPage,
    isNextPageLoading,
    options,
    loadNextPage,
    placeholder,
    onChange,
    value,
  } = props;
  const [selectedOption, setSelectedOption] = useState(value);

  useEffect(() => {
    setSelectedOption(value);
  }, [value]);

  // Extra row to hold a loading indicator if more options are present
  const itemCount = hasNextPage ? options.length + 1 : options.length;

  const loadMoreItems = isNextPageLoading ? () => {} : loadNextPage;

  // Every row is loaded except for our loading indicator row.
  const isItemLoaded = (index) => !hasNextPage || index < options.length;

  const MenuList = ({ children }) => {
    const childrenArray = React.Children.toArray(children);
    // Render an item or a loading indicator.
    const Item = ({ index, style, ...rest }) => {
      const child = childrenArray[index];

      return (
        <div
          className="drop-down-list"
          style={{
            borderBottom: '1px solid rgb(243 234 234 / 72%)',
            display: 'flex',
            alignItems: 'center',
            ...style,
          }}
          onClick={() => handleChange(options[index])}
          {...rest}
        >
          {isItemLoaded(index) && child ? child : `Loading...`}
        </div>
      );
    };
    return (
      <AutoSizer disableHeight>
        {({ width }) => (
          <InfiniteLoader
            isItemLoaded={(index) => index < options.length}
            itemCount={itemCount}
            loadMoreItems={loadMoreItems}
          >
            {({ onItemsRendered, ref }) => (
              <List
                className="List"
                height={150}
                itemCount={itemCount}
                itemSize={35}
                onItemsRendered={onItemsRendered}
                ref={ref}
                width={width}
                overscanCount={4} //The number of options (rows or columns) to render outside of the visible area.
              >
                {Item}
              </List>
            )}
          </InfiniteLoader>
        )}
      </AutoSizer>
    );
  };

  const handleChange = (selected) => {
    onChange(selected);
  };

  return (
    <Select
      placeholder={placeholder}
      components=
      value={selectedOption}
      options={options}
      {...props}
    />
  );
};
export default SelectWrapper;

A significant improvement in the performance of the dropdown is seen.

Need help on your Ruby on Rails or React project?

Join Our Newsletter