React is pretty fast in general, but it can have trouble when repeatedly rendering thousands of elements. This is true even if only a few of the elements are visible at once! UI “virtualization” is a technique for emulating a list with many elements while only rendering as few as possible to make the screen look correct.
Of course, there are plenty of virtualization libraries already available. The most popular is
react-virtualized, which implements just about any layout you could think of and has a ton of customization available. Sometimes you only need something super basic, though, or you have some custom requirements that don’t quite fit with how the library works.
We ran into the latter case last week: we needed to try and add virtualization to a UI component from another framework, and it turned out to be a lot simpler than I assumed!
In this article I’ll show how to build a simple scrollable virtualized list in React, but the basic technique should be applicable to any UI framework.
I’ve set up some examples at nyctef.github.io/virtualized-list-examples where you can play around with different versions and see the source code for each implementation. The 1,000 element case renders reasonably well on my work machine — if you want to see some slowdown, you may want to change the number of elements to 10,000 or enable CPU throttling in Chrome:
The way that most virtualized list components work is that instead of passing a list of elements to render, we instead provide the list with just the number of elements we want to render, how big each element is, and a callback which renders a single item.
The first thing we need to change is how the elements in the list are laid out. Normally we’d create a lot of divs next to each other and let the layout engine stack them up, but now we’re going to be skipping most of the elements. Instead, we’ll position each element absolutely, and force the inner container to be the right height so the scrollbar will still render correctly.
We could wrap each item in a new div to give it the right
top values. However, most virtualization libraries pass some styles into the item render method, which consumers then need to apply to their own elements. This is slightly more efficient at the cost of being more complicated, so you can decide whether you want to implement this yourself or not.
Now we have a list of items we need to display. We have the index of each item, and we know how tall the items are. We need to calculate the indexes of the items which should be visible. For that, we’ll need three bits of information:
The “inner” height is the total height of the list itself. In our simple case, it’s the item height multiplied by the number of items.
The “window” height is the height of the scrollable area: a window into the full list. This height will depend on the surrounding elements. For now it’s easiest to hardcode it to a specific value; we can look into ways of calculating it later.
scrollTop measures how far the inner container is scrolled. It’s the distance between the top of the inner container and its visible part.
To find the elements which intersect the top and bottom edges of our scrollable area, we divide their pixel position by the height of the elements. We then use
Math.floor() to turn the pixel position into a valid element index, and render all the elements between those two indexes:
This is a very basic example: there are many potential improvements you might want to make depending on your requirements. Here are a selection:
- Currently we’re making the assumption that all items in the list are the same size, and passing in a constant
itemHeightvalue. Instead we could make
itemHeightaccept a callback which queries the item height for each index. This makes calculating the visible items a lot more complicated, though — I guess you’d need to build up an index of
pixel offset -> itemindex(probably a binary search tree?). Some libraries offer a second callback which invalidates the saved item heights, so items can change size over time.
- If you turn on CPU throttling and look at the virtualized example, you may notice it’s possible to scroll to elements before they’re rendered, momentarily leaving ugly-looking white space. Many libraries implement “overscan” to solve this problem, which renders additional elements above and below the visible elements, so they’ll already be visible when they scroll into view.
This can also be useful if items have some kind of lazy-loading — we can start loading for elements just outside the viewable area so that they’ll be ready to display when the user scrolls down.
- We’re also assuming that the scrollable area has a fixed size. We still need to know what this size is, but we can get this information automatically using
ResizeObserverinto a HOC or hook is probably a good idea in React; alternatively you can implement this behavior directly in your virtualized list.
Of course there’s many more tweaks you can make. If you find yourself implementing a lot of these features, you may want to look into using a library yourself (or creating your own!). But being able to build a custom implementation yourself that does exactly what you need is very powerful.
Have fun! :)
References I found useful:
- https://jsfiddle.net/1wtnfcgq/6/ Not sure who wrote this, but thanks!