It’s important to keep in mind that although browsers can handle larger DOM trees, it’s advised to limit the total DOM nodes count to 1500, the DOM depth to 32, and the DOM node count for a single parent element to 60.
We can end up with an excessive DOM size by either sending a sizable HTML file over the wire or by generating elements at runtime until we exceed the performance budgets.
When displaying a large set of data, there are many ways we can implement the visualization. The most notable ways to render the data set are via as-is, pagination, or infinite scrolling.
We can visualize these three options as such:
When we have continuous content, such as multiple paragraphs, on our page, we would use the “as-is” strategy to render our content. To optimize our page performance, we recourse to the CSS content-visibility property. See this blog post for more details.
However, using content-visibility would only help on the initial render. When we scroll down the page to the areas that the browser skipped rendering, we will end up with a slow-moving page again.
The same is true for infinite scrolling. The difference is that we only request content as it’s needed. However, we will eventually experience the same sluggish performance issues.
Pagination, on the other hand, is the most performant way to render. It displays only the necessary content on the initial render, it requests content as needed, and the DOM never bloats with needless content.
But, pagination is a pattern that isn’t suitable for displaying every large data set on a webpage. Instead, we can use virtualization.
Virtualization is a rendering concept that focuses on tracking the user’s position and only committing what is visually relevant to the DOM in any given scroll position. Essentially, it provides us with all the benefits of pagination along with the UX of infinite scrolling.
To virtualize a list, we pre-calculate the total height of our list using the dimensions of the given list items and multiplying it by the count of our list items.
Then, we position the items to create a list that the user can scroll through. Positioning our elements correctly is key to the efficiency of virtualization because individual items can be added or removed without affecting other items or causing them to reflow (i.e., the process of re-calculating an element’s position on the page).
However, there’s another way to render data.
To install react-window, run the following:
react-window will be installed as a dependency, while the types for it will be installed as a devDependency even if we’re not using TypeScript. We will also need faker.js to generate our large data set.
In our App.js, we will import faker as well as useState, and initialize our data state with faker’s address.city function. In our code, it will create an array with a length of 10000.
Next, we lazily initialize our state using a function to optimize for performance. Then, we make our list scrollable by giving it a width and a height and setting overflowY to scroll.
To compare the performance with and without virtualization, we will add a reverse button that reverses our data array.
Now, try the reverse button and notice how latent the update is.
To virtualize this list, we will be using react-window’s FixedSizeList.
We can use FixedSizeList in multiple ways. In this instance, we are creating an imaginary array with the same length of our data (through itemCount) and using it to index our data.
FixedSizeList’s children expose a render prop that has each index and the necessary styles (absolute positioning styles, etc.) passed into it.
We can also be explicit and pass our data and receive it in the render prop through itemData, like so:
Notice that our inline styles from earlier are now replaced with width and height props. overflowY is controlled by the layout prop, which defaults to vertical.
It’s important to pass the style render prop argument to the outermost element (the li, in our case). Without it, all elements will stack on top of one another and there will be nothing to scroll through.
The FixedSizeList elements render two wrapper elements that both default to divs and can be customized using innerElementType and outerElementType.
In our case, we set innerElementType to ul for accessibility reasons. However, only predefined props can be used. Adding props such as role or data-* will not have any effect.
By default, FixedSizeList will use the data indices as React keys. But because we are modifying our data array, we must use unique values for our keys. For that, FixedSizeList exposes the itemKey prop, which takes a function that should return either a string or a number. We will be using faker’s datatype.uuid function.
As we mentioned, we can instantaneously compare our virtualized list to the non-virtualized list using the reverse button. But the performance optimizations do not end there. If we have an expensive element that we render per each list item instead of our single li, react-window allows us to render a simple UI instead when scrolling.
To do this, we first need to enable the isScrolling boolean by passing useIsScrolling to our FixedSizeList.
Here’s what that could look like:
Now that we know how to virtualize a list, let’s learn to virtualize a grid. It’s a similar process, but the difference is that you have to add your data’s count and dimensions in both directions: vertically (columns) and horizontally (rows).
In this article, we covered the performance limits of the DOM as well as how to optimize a lean DOM using multiple rendering strategies. We also discussed how virtualization, through the use of react-window, can efficiently display large data sets to meet our performance targets.