October 4, 2016

Optimizing React Rendering through Virtualization

By Juho Vepsäläinen | 5 min read

Optimizing React Rendering-through Virtualization

Even though React is fairly performant out-of-the-box, sometimes you need to tune it.

The most common trick is to implement shouldComponentUpdate lifecycle method so that React can skip rendering based on a custom check. This can be convenient if equality checks against the data happen to be cheap (i.e. you are using some library providing immutable structures).

Sometimes this isn’t enough. Consider rendering thousands of lines of tabular data. It can quickly become a heavy operation even if you have nice checks in place. That is when you need to be more clever and implement a context specific optimization.

Protect your React App with Jscrambler

Given tabular data fits well within a viewport (think CSS overflow), it is possible to render only the visible rows and skip rendering the rest. This technique – virtualization – is worth understanding in detail as it gives you greater insight into React.

Basic Idea of Virtualization

The biggest constraint of virtualization is that you need a viewport of some sort. You could treat even the browser window as such. Often, however, you set up something more limited. A simple way to achieve this is to fix width/height of a container and set overflow: auto property through CSS. The data will then be displayed within the container.

The question is, how to figure out what to render? The naïve case is simple. Assuming we are rendering table rows, we will render initially only as many as fit within the viewport. Let’s say the viewport is 400 pixels tall and our rows each take 40 pixels. Based on that we can say we can render up to ten rows at first.

Problem of Scrolling

The problems begin once the user starts to scroll. We can capture scrolling related information using a onScroll handler. Within there we can dig the current y coordinate of scroll top position through e.target.scrollTop.

Let’s say the user scrolled for 50 pixels vertically. Quick math tells us we should skip rendering two rows (50 – 20 * 2 = 10) and start rendering from the third. We should also offset the content by 30 pixels (50 – 20 = 30). One way to handle the offset is to render an extra row at the beginning and set its height accordingly.

There is also a similar problem related to the rows at the end. Since we’ll want to skip rendering rows there too, we’ll need to perform similar math. This time we’ll figure it out based on the viewport height while taking offset into account. To get the scrollbar height right, we can render an extra row using this information.

Based on these calculations and a bit of extra work we can figure out the following things:

  • Extra padding at the beginning – extra row to make scrollbar look right.
  • Amount of rows to render including location – these should be sliced from the whole data set.
  • Extra padding at the end – extra row to make scrollbar look right.

Problem of Indices

This isn’t everything. There are a couple of gotchas in the scheme. If we have styled our content with even/odd kind of styling through CSS, you will see that something is wrong quite fast. A basic algorithm will lead to flickering as you scroll.

The flickering has to do with the fact that we are always rendering from “zero” without taking actual indexing into account. Fortunately this is an easy problem to fix. Once you know where you are slicing the data from, you can check whether the starting index is even or not. If it’s even, render an extra row at the start. It can have zero height. This little cheat makes the rendering result stable and gets rid of flickering.

Problem of Heights

In the example above we made it easier for ourselves by assuming that row height is set to some specific value. This is the most common way to approach it and often enough. What if, however, you allowed arbitrary row heights?

This is where things get interesting. It is still possible to implement virtualization, but there is an extra step to perform – measuring.

Idea of Measuring

One way to solve this problem is to handle it through a React feature known as context and callbacks to update the height information at higher level where the logic exists. Depending on your design, regular props could work as well, or you could use a state management solution for this part. No one right way.

The idea is that once componentDidMount or componentDidUpdate lifecycle method of a row gets called, you’ll trigger the callback with the row height information. You can capture this using a ref and offsetHeight field.

You may also want to pass id information related to the row to the callback. This allows you to tell the measurements apart. It’s better to use the actual rowid over index as latter won’t yield predictable results.

Getting Initial Measurements

Beyond measuring, you’ll need to use the measurement data for figuring out the same we did above. The math is a notch harder, but the ideas are the same. The biggest difficulty is actually handling the initial render. You don’t have any measurement data there and you need to get it somehow.

I ended up handling this problem by implementing componentDidMount and componentDidUpdate lifecycle methods at the logic level. It is important to note that if you use an inline CSS solution such as Radium, the initial measurement data might be invalid! The solutions take a while to apply their styling and as a result React’s lifecycle methods might not work as you expect.

To give you a rough idea of how I solved this, I ended up with the code below:

class VirtualizedBody extends React.Component {
    componentDidMount() {
            // Trigger measuring initially
        componentDidUpdate() {
            // Called due to forceUpdate -> cycle continues if
            // there are no measurements yet
    checkMeasurements() {
        if (this.initialMeasurement) {
            const rows = calculateRows(...);

            if (!rows) {

            // Refresh the rows to trigger measurement
            setTimeout(() => {
                    () => {
                        // Recalculate rows upon completion
                            /* If calculateRows returned something,
                            (not {}), finish*/
                            () => this.initialMeasurement = false
            }, 100); // Try again in a while

export default VirtualizedBody;

It might not be the most beautiful solution, but it operates within the given constraints. Most importantly it allows us to capture initial measurement data that in turn can be extrapolated to give us average row height. The more data we render, the better average we can get.

In a scheme like this we will be most often be dealing with incomplete data. In practice, having something that’s good enough is valuable. Especially if you are rendering thousands of rows, small inaccuracies won’t affect scrollbar rendering much.

The most accurate result can be gained by measuring each row, but that in turn would eat our performance. It would most likely be possible to implement more sophisticated measuring to gather the data on the background, but so far a rough approximation such as the one above has proven to be enough.


Even though virtualization doesn’t feel like a hard problem on paper, it can be difficult to handle when you get to the browser. This is particularly true if you loosen the constraints and allow arbitrary row heights. You can turn a relatively simple problem into a much harder one this way. From the user point of view not having to specify height can be nice even if it’s trickier to implement.

The greatest advantage of virtualization is that it cuts down the amount of rendering substantially. Often viewports are somewhat limited. Instead of thousands of rows, you will end up rendering tens of rows at the worst case. That is a massive difference even if you skip performing shouldComponentUpdate per row. The need for that simply disappears.

You can see this particular approach in action at my table component, Reactabular. See react-virtualized for another take on the topic.

If you're building React applications with sensitive logic, be sure to protect them against code theft and reverse-engineering by following our guide.

Juho VepsäläinenI am behind the SurviveJS effort. In addition to being a core developer of webpack, I have been active in the open source scene since the early 2000s.
View All Posts

Subscribe to our weekly newsletter

Learn more about new security threats and technologies.

I agree to receive these emails and accept the Privacy Policy.
Projeto Co-Financiado por (Mais info)Norte 2020, Portugal 2020, FEDR