Introduction
Rendering large lists in React can be a challenging task for developers. As the size of the list grows, the DOM (Document Object Model) tree also grows, leading to performance issues like slow rendering, janky scrolling, and high memory usage. In this article, we will discuss some of the common problems that developers face while rendering large lists, and various solutions that can be used to overcome them.
Problem Statement
Why is rendering a massive list in a web browser a challenging task? Well, there are a few factors to consider while rendering a massive list of items. Firstly it’s the performance, as the number of elements to render on the screen increases, the browser’s rendering engine starts hitting performance issues. This would lead to slow rendering, resulting in a sluggish user interface and poor user experience.
Manipulating a massive list of elements will be computationally expensive. Scrolling through a large list can be janky for your end users, and in the worst cases, they could end up with a completely unresponsive page. The negative impact on low-end devices like mobile phones would be even greater due to limited processing power and memory.
With all these issues considered, we as software developers need to employ optimization techniques and choose appropriate strategies.
Summary
This article revolves around rendering massive lists in React. Firstly we’ll define what we mean by a massive list. Then we’ll look into some possible solutions you can incorporate to solve the issues pointed out in the problem statement. Finally, we’ll jump into a few key things to keep in mind while rendering a massive list in browsers.
What do we mean by a massive list?
The definition of a massive list is dynamic. It depends entirely on your end user’s device. If they are all on high-end computers or mobile devices, the number of elements you can render safely on your page before seeing any performance throttling would be significantly high.
On the other hand, if your end user’s device is more modest with limited memory and processing capabilities, this threshold would be smaller than the one above. With the advancement in modern browsers, you can easily load a vast amount of DOM elements without having to worry much, but even so, if you start rendering a list of orders with 1,000 or more rows, you can consider it to be massive.
How to Tackle Rendering Large Lists in React?
If you end up in a place where you have to render tens of thousands of complex list items, you would inevitably have to use one of the following solutions, no matter how capable your end user’s hardware is. Firstly we’ll start with a general solution that everyone can incorporate into their code:
Keep Your Render Tree Small
In general, keeping a lean render tree boosts overall performance, because the more DOM elements there are, the more space needs to be allocated by your browser. Additionally, more time will be taken by the browser in the layout stage. This becomes extremely critical while rendering a massive list. Reducing even a single div can bring out an observable difference in terms of performance.
The best practice would be to use the minimum number of DOM elements to create your list item without affecting the design.
Let’s experiment:
First, we’ll create a simple list of around 10,000 elements. Here’s the code sandbox for the same.
Let’s look into the Lighthouse performance of the page:
First contentful paint | 0.9 seconds |
Largest contentful paint | 3.0 seconds |
Total blocking time | 2.1 seconds |
Total performance score | 43 |
Now, let’s add one more div to the list item and get all the performance numbers.
Here’s the Lighthouse performance:
First contentful paint | 1 second |
Largest contentful paint | 3.3 seconds |
Total blocking time | 3.3 seconds |
Total performance score | 40 |
Infinite Scroll
Now let’s jump into actual solutions, the first of which is using the infinite scroll technique. In simpler words, this technique means to only render the list items required to fill in the entire page length, and then add more items as the user scrolls down.
We can implement this using the react-infinite-scroller library.
Here’s the implementation of the same 10,000-item list using react-infinite-scroller.
Let’s see how it affects our performance:
First contentful paint | 1.0 seconds |
Largest contentful paint | 2.5 seconds |
Total blocking time | 160ms |
Total performance score | 78 |
Our total performance scroll jumped from a base of 43 to 78, which is a massive gain.
This is because of the fact that we essentially chopped our list into tiny portions. Instead of rendering all 10,000 items at once, we only rendered the first few items needed to render the portion of the page in the user’s view. When the user scrolls down, the react-infinite-scroller library adds more items to the DOM.
Windowing
Another solution to this problem is to use the windowing technique.
Windowing (or virtualization) is a technique where you only render the portion of the list that is visible to the user at any time. Unlike infinite scroll, in windowing the DOM always has a constant number of elements. In other words, we only render the required elements needed to fill the user’s field of vision and remove the elements from the top and bottom of the list that are not in the view yet.
Windowing shines when each item in your list has a fixed height. That way it becomes easy to calculate which items need to be added or removed from the DOM based on the scroll. Furthermore, the memory usage of your page stays constant, as the number of DOM elements stays constant, no matter where the user has scrolled.
Here’s the implementation of the same 10,000-item list using react-window.
Let’s see how it affects our performance:
First contentful paint | 1.0 seconds |
Largest contentful paint | 2.5 seconds |
Total blocking time | 190ms |
Total performance score | 76 |
Key Things to Keep in Mind
With both these techniques, you lose the capability to perform a browser search. In other words, if the user performs a cmd + f search for a text that hasn’t been rendered yet, they’ll see 0 of 0 matches. To prevent this, you should add custom search or filtering options to your lists.
There will also always be minor delays in adding/removing elements from the DOM, as compared to native scrolling, but that’s still better than your application ultimately hanging due to massive amounts of DOM elements.
Creating Your Own Solution
If you want to keep your solution lightweight and don’t want to use any libraries, you can also implement your own lazy loading of DOM elements by using an Intersection Observer API.
First, add enough components to fill up your viewport. Get the viewport height using getBoundingClientRect and divide that by your approximate minimum item height. Add 1 to this count for good measure. Now add a dummy component below it (this component doesn’t need to show anything; it just needs to exist at the bottom of your list). Attach the intersection observable to this component, and whenever this component is in view, add more items to your list to be rendered in the DOM. This way you’ll end up developing a simple infinite-scroll solution.
Conclusion
You should now be able to tackle large lists in a React application. In simple terms, you should only render a small portion of the list at a time and make sure there are enough filters present in the view for the users to slice and dice through the list.
Srijan Gulati
Srijan Gulati is a Senior Software Developer and the Frontend lead for Uber’s Crash analytics (Apps, services, and in-app bug reporter). He is passionate about responsive and accessible UX, problem-solving, and VIM.
Karan Verma
Karan Verma is a Senior Software Engineer and Frontend Developer specializing in data visualization at scale.
Posted by Srijan Gulati, Karan Verma
Related articles
Most popular
Streamlining Financial Precision: Uber’s Advanced Settlement Accounting System
Shifting E2E Testing Left at Uber
Uber, Unplugged: insights from 6 transit leaders on the future of how we move
Continuous deployment for large monorepos
Products
Company