Robust and snappy applications are no longer a luxury in today’s world — it is a minimum requirement to stay competitive.
60 FPS is the minimum required for a delightful user experience. In just a few seconds, users can lose interest in a slow application, close the tab, and never return to it. It is, therefore, crucial to continue investing in your application’s performance as you scale it.
In this post, we will focus specifically on optimizing the performance of large lists of data using two techniques: windowing and component recycling.
Almost any app has lists of some sort, and they oftentimes also contain lots of data. So you must ensure that the user interface does not lag or crash, no matter how much data your lists display.
To top it off, displaying lists of bare text is no longer enough. Modern applications include lists with interactive elements, like buttons and hover effects that are expensive to render and can quickly degrade the performance, even when displaying data of moderate size.
So how do we display large lists while keeping both, the performance we desire and all of the features we want?
In React, we have two well-known solutions for that: windowing and recycling.
Both techniques aim to make your lists lighter and faster to render, but their approaches differ. So we’ll cover windowing and component recycling and their tradeoffs in more detail.
So far, we’ve established that displaying a large number of elements can slow down your application pretty quickly. But what if we don’t necessarily need to render all the elements? That is the main idea behind windowing.
Windowing is a technique that ensures that our lists only render items visible in the viewport.
As the user scrolls, we calculate which elements we should display based on the position of the scrollbar and then add and remove those elements as they enter and exit the viewport.
Rendering DOM elements is one of the most expensive operations web applications perform, and with windowing, we ensure that we render them only when needed.
In React, we have two popular libraries for implementing windowing: react-window and react-virtualized. Both libraries are created and maintained by the same author, bvaughn.
Out of the two, react-window is the newer, simpler, and more lightweight choice, and it’s the one we’ll be using for this tutorial. react-virtualized is larger and more feature-rich and is worth considering if you run into hard limitations with react-window.
react-window is a great choice to implement windowing, and it is often paired with react-virtualized-auto-sizer and react-window-infinite-loader libraries to build modern lazy-loading lists that fill the height of the parent component.
Now let’s see how we would go about implementing a simple list with react-window.
N.B., react-window supports both fixed-size and variable-size lists. Both types require you to specify the item height before it renders. The main difference is that with the fixed-size list, all of your rows will have the same height, while with the variable-size list, you have the freedom to set different heights for each item on the fly.
Okay, now let’s see what the implementation of a variable height list looks like:
import React from "react"; import { VariableSizeList } from "react-window"; import AutoSizer from "react-virtualized-auto-sizer"; const Row = ({ index, style }) => <div style={style}>Row: {index}</div>; export const ReactWindowList = () => ( <AutoSizer> {({ height, width }) => ( <VariableSizeList className="List" height={height} itemCount={1000} itemSize={() => 35} width={width} > {Row} </VariableSizeList> )} </AutoSizer> );
In this example, you can see how we’re pairing react-window with react-virtualized-auto-sizer. The library provides the AutoSizer
component, which calculates the height and width of its parent, and we use those values in our list.
We do this to ensure that the list takes up 100% of the height of its parent. And since react-window does not provide the autosizing feature on its own, you need to pair it with react-virtualized-auto-sizer. Luckily, both libraries were created by the same author, so they are specifically designed to work well together.
If you inspect the row elements in your list using browser tools, you will notice that they are destroyed and re-created as the user scrolls. That’s the result of the windowing technique.
The next logical step to further improve your list’s performance is to implement lazy loading. With lazy loading, instead of fetching all of the data in advance, you fetch the next set of items dynamically, as the user scrolls through your list. It’s a form of pagination. And as you might have guessed, there is yet another package that lets you do just that: react-window-infinite-loader.
With react-window and react-window-infinite-loader, putting together an infinite lazy-loading list that fetches data as you scroll is rather easy:
Here’s the link to the codesandbox project shown above.
But react-window lets you implement more than just simple lists. The library supports implementing performant grids, with rows and columns created dynamically using the windowing technique:
import React, { forwardRef } from "react"; import { FixedSizeGrid as Grid } from "react-window"; const COLUMN_WIDTH = 100; const ROW_HEIGHT = 35; const Cell = ({ columnIndex, rowIndex, style }) => ( <div className={"GridItem"} style={{ ...style, }} > r{rowIndex}, c{columnIndex} </div> ); export const ReactWindowGrid = () => ( <Grid className="Grid" columnCount={50} columnWidth={COLUMN_WIDTH} height={150} innerElementType={innerElementType} rowCount={100} rowHeight={ROW_HEIGHT} width={300} > {Cell} </Grid> ); const innerElementType = forwardRef(({ style, ...rest }, ref) => ( <div ref={ref} style={{ ...style, }} {...rest} /> ));
Notice that in this example, since we used the fixed grid, we had to specify a constant value for width and height. But the library also provides the variable grid variant that allows you to specify those values on the fly.
Hopefully, this example demonstrates how easy it is to add windowing to your list using react-window.
We really didn’t have to configure much and got an almost out-of-the-box solution using the react-window and react-virtualized-auto-sizer libraries.
Now let’s talk about component recycling.
Component recycling is another effective technique for improving the performance of large lists. The idea behind recycling is similar to windowing in that we’re trying to lower the amount of DOM elements our lists produce. However, how we go about achieving it is different.
Recycling works by reassigning the key of an existing DOM element to a new one that’s about to render. Thus, React will not trigger the unmounting and mounting events and instead will simply update the props of the existing row item with the new data using componentDidUpdate
.
Compared to windowing, recycling is more performant because you don’t need to delete and recreate DOM elements each time.
But the trade-off is that it can have unexpected side effects on the render cycle of your components.
There are multiple libraries out there to help you properly implement component recycling for your lists, with recyclerlistview and flash-list being just a couple of examples.
Now let’s see what the component recycling implementation using recyclerlistview
looks like:
import React, { Component } from "react"; import { View, StyleSheet } from "react-native"; import { RecyclerListView, DataProvider, LayoutProvider, } from "recyclerlistview"; import { LayoutUtil } from "./utils/LayoutUtil"; const getItems = () => { const array = []; for (let index = 0; index < 1000; index++) { array.push(`Row item ${index}`); } return array; }; const layoutProvider = new LayoutProvider( () => { return "VSEL"; //Since we have just one view type }, (type, dim, index) => { const columnWidth = LayoutUtil.getWindowWidth() / 3; dim.width = 3 * columnWidth; dim.height = 300; } ); export default class App extends Component { constructor(props) { super(props); this.state = { dataProvider: new DataProvider((r1, r2) => { return r1 !== r2; }), layoutProvider, images: [], count: 0, viewType: 0, }; this.inProgressNetworkReq = false; } componentWillMount() { this.setData(); } async setData() { this.setState({ dataProvider: this.state.dataProvider.cloneWithRows(getItems()), }); } rowRenderer = (type, data) => { console.log({ data }); return ( <div style={{ background: "gray", height: 280 }}> Row Item {data.text} </div> ); }; render() { return ( <View style={styles.container}> <RecyclerListView style={{ flex: 1 }} contentContainerStyle={{ margin: 3 }} onEndReached={this.handleListEnd} dataProvider={this.state.dataProvider} layoutProvider={this.state.layoutProvider} renderAheadOffset={0} rowRenderer={this.rowRenderer} /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: "stretch", justifyContent: "space-between", }, });
Immediately, you can tell that the recyclerlistview
implementation is a bit more complex.
Seems like a lot is happening in this code, but the most important parts to focus on are DataProvider
, LayoutProvider
, and rowRenderer
.
As their names suggest, the rowRender
function defines how the row items are rendered, DataProvider
is in charge of supplying the data for the list, and LayoutProvider
provides the styling attributes for the display of each item, such as height and width.
recyclerlistview
also allows implementing lazy loading by subscribing to the onEndReached
event and specifying a custom footer component to show a loader while more data is fetched.
Like react-window, recyclerlistview
also allows the creation of complex grids and lets you customize the UI for each of their cells.
To find the complete list of properties and customizations, refer to this table from their docs.
Since component recycling is more performant, does it mean we should always choose it over windowing? Not necessarily. While it is more performant than windowing, component recycling might not always be the best choice.
From my experience, windowing tends to work better for variable height rows since it dynamically re-creates the DOM elements as the user scrolls. On top of that, windowing is often simpler to implement and offers performance results that are comparable to recycling and should be enough for most applications.
Also, as I mentioned before, due to the nature of the approach, recycling could have unexpected side effects on the lifecycle methods of your components.
When choosing the best performance optimization technique, remember the law of diminishing returns.
If you’re unsure which to choose, try a simpler technique first, then move on to a more complex one if you need further performance tuning. So, following that advice, I suggest first trying out windowing and if that’s not sufficient, try component recycling.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]