Virtualizing The Virtual DOM — Pushing React Further
Every app has a list: Facebook, Gmail, Twitter, LinkedIn, Pokémon Go… (you got it) and OutSystems apps are no exception.
While developing some of the apps with OutSystems Platform 10 we started hitting some performance problems when dealing with large sets of data. Read more about it here. After applying all the “tricks” that React had to offer, we were still lagging a bit behind when compared with native apps.
Our apps showed 2 major problems: high rendering times and out-of-memory errors (on infinite scroll lists). After some profiling it became obvious that we needed to decrease the number of components.
React Components overhead
A React component has some significant memory and speed overhead (noticeable above thousands). Version 0.14 introduced Functional components — “components that are pure functions of their props.”. These components are intended to be faster and lighter although after some reading and experiments, we found no visible performance gains as of version 15. Still, they have plans to do some optimizations in the future.
So we had to find alternatives to solve this problem.
Out of sight, out of… DOM
Browsers already do some rendering optimizations, skipping rendering of regions outside the viewport. However, all the page DOM, React virtual DOM and all components live in memory. Additionally, you still have to pay the price to build and run components’ lifecycle functions.
It would be great if we could skip creating and rendering all the components that are outside viewport. Even though it’s not easy to accomplish that in React, today.
Back in February, at the React Conf 2016 in San Francisco, I heard a member from the React core team talking about their future plans to implement windowing (render only what’s visible) and the challenges ahead to bring layout information to render due to the lack of browsers’ APIs to properly measure text. But this is the future, and we had to solve the problem yesterday (like always).
Reinventing the wheel
Implementing the list virtualization (or windowing) solution in React feels like reinventing the wheel.
We don’t like to reinvent the wheel… so we googled for someone who had already solved these challenges. We found several solutions but none would fulfill our requirements:
- No extra html elements
- Support variable list item sizes (not known beforehand)
- Support enter and leave animations
Without no options left, we had to reinvent the wheel…
Now you see it… now you don’t
To implement virtualization on a list, you have to listen for scroll events on the scroll container (the element that overflows) and remove elements (list items) as they go out of the viewport and add as they go in.
For instance: if you have a list with 1000 items and only 20 fit the viewport, only 20 will be rendered (each time), the others (outside viewport) remain on hold.
Rendering some extra items helps with maintaining good scroll performance and prevent some blank spaces from appearing. Our tests showed that rendering an extra viewport of items seems to be a good balance between performance and good behavior.
For the sake of writing, let’s consider a vertical list, items that stack vertically on top of each other — although the same principle applies to horizontal stacking lists.
As items on top disappear, you have to compensate for their height with the corresponding blank space (as if they were there). Also, you have to create an extra space after the last rendered item to keep the scroll height correct (for scrollbars to behave correctly). This is where things start to get interesting.
In our first approach, we used a wrapper div around all the list items. That div helped keep a correct scroll height as well as gave an extra top padding to pull elements down (compensate for the height of the first non-rendered items).
Then, soon we hit problems with li lists. According to the W3C spec, ul tags only accept li and script as child tags, thus having a div, inside ul, lead to malformed html.
Not only that, but css also broke once we introduced this extra div, because some styling rules weren’t expecting the div.
What about script tags? They’re not visible and don’t occupy space… or do they?
As it turns out, script tags can be visible and forced to occupy space, after setting the proper display value (other than none which is their default) and height.
Et voilá, we can discard our div and use script elements to have the spacer role. Not that I am proud, but the law (spec) and browsers seem to agree.
If the list items have all the same height, scrolling is a trivial task. When scrolling down: remove all the items on top that went out of the viewport and increase the top spacer height with the aggregated sizes of the leaving items. When scrolling up: do the other way around.
What if the list items have variable height?
When scrolling down, since we know the height of the items leaving, calculating the spacer’s new height is a no-brainer.
Scroll up takes a little more work because we don’t know, beforehand, the height of the items entering. To make an educated guess, we keep an updated average of the items’ height to calculate the number of items that will enter (to fill the viewport). After the entering items are rendered, we adjust the spacer height with their actual height. This way the scroll will be smooth, there won’t be jumps.
When the scrolling delta is too big (greater than the viewport size) we use the average items size to calculate the first and last items to display.
Follow React’s lifecycle
Rendering only the visible items requires accessing the browser to obtain information about items’ height and scroll top. We can’t do it on the component’s render function because it would violate React’s best practices and lead to layout trashing problems.
React’s documentation states the following about the render function:
… should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not read from or write to the DOM or otherwise interact with the browser. If you need to interact with the browser, perform your work in componentDidMount() or the other lifecycle methods instead.
Following the React way of doing things, whenever scroll changes, we do all the math that requires access to the browser and store a computation of the relevant information on component’s state which triggers a render. After all the most complex work is done, render just returns the items to be rendered (according to the current state).
Things were looking good at this point. However, once we turned animations on, all items entering and leaving the viewport kept animating, which wasn’t the behavior we wanted.
If only we could turn off animations when scrolling… but looking at React’s CSSTransitionGroup API, it’s not possible.
So we looked at the CSSTransitionGroup’s source code to get some inspiration (unfortunately we couldn’t find a clean way to extend its behavior) and created a new component able to suspend animations.
In a future post, we will get into some details on other cool things that we packed on this animation component.
Show me some code
Tired of all the reading? You can find the component’s code here.
Feel free to fork it and contribute to improve it.
We hope to see some improvements on React’s side in the future to help developers solve these kind of problems easily and with less effort.
Until then, using the virtualization technique, you’ll be able to have lists with thousands of items that render in a blink of an eye (as if they had only some tens of elements). Infinite scroll patterns also get faster and more robust (no more out-of-memory problems).
Editor’s Note: João Neves is a React expert who lives and breathes performance. His been relentlessly cutting milliseconds from mobile apps built with OutSystems.