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.

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 -
- react-window-infinite-loader - Lazy loading
- react-window- Renders a part of a large data set
- react-virtualized-auto-sizer - Automatically adjusts the width and height of a component
- 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.
