Optimizing React: Virtual DOM explained

Cover for Optimizing React: Virtual DOM explained

Topics

Share this post on


Translations

If you’re interested in translating or adapting this post, please contact us first.

Learn about React’s Virtual DOM and use this knowledge to speed up your applications. In this thorough beginner-friendly introduction to framework’s internals, we will demystify JSX, show you how React makes rendering decisions, explain how to find bottlenecks, and share some tips to avoid common mistakes.

One of the reasons React keeps rocking the frontend world and shows no sign of decline is its approachable learning curve: after wrapping your head around JSX and the whole ”State vs. Props” concept, you are good to go.

But to truly master React, you need to think in React. This article is an attempt to help you with that. Take a look at the React table made for one of our projects:

A huge React table

A huge React table on eBay for business

With hundreds of dynamic, filterable rows, understanding framework’s finer points becomes essential to guarantee smooth user experience.

And you can certainly feel when things go wrong. Input fields get laggy, checkboxes take a second to be checked, modals have a hard time showing up.

To be able to solve these kinds of problems, we need to cover the whole journey a React component takes from being defined by you to being rendered (and then updated) on a page. Buckle up!

Behind JSX

React developers urge you to use a mix of HTML and JavaScript known as JSX when you write your components. Browsers, though, have no clue about JSX and its syntax. Browsers only understand plain JavaScript, so JSX will have to be transformed into it. Here is the JSX code for a div that has a class and some content:

<div className='cn'>
  Content!
</div>

The same code in “formal” JavaScript is just a function call with a number of arguments:

React.createElement(
  'div',
  { className: 'cn' },
  'Content!'
);

Let’s take a closer look at these arguments. The first one is a type of element. For HTML tags it will be a string with a tag’s name. A second argument is an object with all of the element’s attributes. It can also be an empty object if there are none. All the following arguments are element’s children. Text inside an element also counts as a child, so a string ‘Content!’ is placed as the third argument to a function call.

You can already imagine what happens when we have more children:

<div className='cn'>
  Content 1!
  <br />
  Content 2!
</div>
React.createElement(
  'div',
  { className: 'cn' },
  'Content 1!',              // 1st child
  React.createElement('br'), // 2nd child
  'Content 2!'               // 3rd child
)

Our function now has five arguments: an element’s type, an attributes object, and three children. As one of our children is also an HTML tag known to React, it will be portrayed as a function call as well.

By now, we have covered two types of children: a plain String or another call to React.createElement. However, other values can also serve as arguments:

  • Primitives false, null, undefined and true
  • Arrays
  • React components

Arrays are used because children can be grouped and passed as one argument:

React.createElement(
  'div',
  { className: 'cn' },
  ['Content 1!', React.createElement('br'), 'Content 2!']
)

The power of React comes, of course, not from the tags described in the HTML spec, but from the user-created components such as:

function Table({ rows }) {
  return (
    <table>
      {rows.map(row => (
        <tr key={row.id}>
          <td>{row.title}</td>
        </tr>
      ))}
    </table>
  );
}

Components allow us to break our templates into reusable chunks. In the example of a “functional” component above we accept an array of objects holding table row data and return a single React.createElement call for a <table> element and its rows as children.

Whenever we place our component into a layout like so:

<Table rows={rows} />

from the browsers’ perspective, we wrote this:

  React.createElement(Table, { rows: rows });

Note that this time our first argument is not a String describing an HTML element, but a reference to a function that we defined when we coded our component. Our attributes are now our props.

Putting components on a page

So, we have transpiled all our JSX components into pure JavaScript, and now we have a bunch of function calls with arguments that are other function calls, holding yet other function calls… How does it all get transformed into DOM elements that form a web page?

For that, we have a ReactDOM library and its render method:

function Table({ rows }) { /* ... */ } // defining a component

// rendering a component
ReactDOM.render(
  React.createElement(Table, { rows: rows }), // "creating" a component
  document.getElementById('#root') // inserting it on a page
);

When ReactDOM.render is called, React.createElement is finally called too and it returns the following object:

// There are more fields, but these are most important to us
{
  type: Table,
  props: {
    rows: rows
  },
  // ...
}

These objects constitute Virtual DOM in React’s sense.

They will be compared to each other on all further renders and eventually translated into a real DOM (as opposed to virtual).

Here’s another example: this time with a div that has a class attribute and several children:

React.createElement(
  'div',
  { className: 'cn' },
  'Content 1!',
  'Content 2!',
);

Turns into:

{
  type: 'div',
  props: {
    className: 'cn',
    children: [
      'Content 1!',
      'Content 2!'
    ]
  }
}

Note that children who used to be separate arguments to the React.createElement function have found their place under a children key inside props. So it does not matter whether children are passed as an array or a list of arguments—in a resulting Virtual DOM object they will all end up together anyway.

What’s more, we could add children to the props directly in the JSX code, and the result would still be the same:

<div className='cn' children={['Content 1!', 'Content 2!']} />

After a Virtual DOM object is built, ReactDOM.render will try to transform it into a DOM node our browser can display according to those rules:

  • If a type attribute holds a string with a tag name—create a tag with all attributes listed under props.

  • If we have a function or a class under type—call it and repeat the process recursively on a result.

  • If there are any children under props—repeat the process for each child one by one and place results inside the parent’s DOM node.

As a result, we get the following HTML (for our table example):

<table>
  <tr>
    <td>Title</td>
  </tr>
  ...
</table>

Rebuilding the DOM

Note that “re” in the heading! The real magic in React starts when we want to update a page without replacing everything. There are few ways how we can achieve it. Let’s start with the simplest one—call ReactDOM.render for the same node again.

// Second call
ReactDOM.render(
  React.createElement(Table, { rows: rows }),
  document.getElementById('#root')
);

This time, the piece of code above will behave differently to what we’ve already seen. Instead of creating all DOM nodes from scratch and putting them on the page, React will start the reconciliation (or “diffing”) algorithm to determine which parts of the node tree have to be updated and which can be left untouched.

So, how does it work? There is just a handful of simple scenarios and understanding them will help us a lot with our optimization. Keep in mind that we are now looking at objects that serve as a representation of a node in the React Virtual DOM.

Scenario 1: type is a string, type stayed the same across calls, props did not change either

// before update
{ type: 'div', props: { className: 'cn' } }

// after update
{ type: 'div', props: { className: 'cn' } }

That is the simplest case: DOM stays the same.

Scenario 2: type is still the same string, props are different

// before update:
{ type: 'div', props: { className: 'cn' } }

// after update:
{ type: 'div', props: { className: 'cnn' } }

As our type still represents an HTML element, React knows how to change its properties through standard DOM API calls, without removing the node from a DOM tree.

Scenario 3: type has changed to a different String, or from String to a component

// before update:
{ type: 'div', props: { className: 'cn' } }

// after update:
{ type: 'span', props: { className: 'cn' } }

As React now sees that the type is different, it would not even try to update our node: old element will be removed (unmounted) together with all its children. Thus, replacing an element for something entirely different high up the DOM tree can be quite expensive. Luckily, that rarely happens in the real world.

It is important to remember that React uses === (triple equals) to compare type values, so they have to be the same instances of the same class or the same function.

Next scenario is much more interesting, as that is how we use React most often.

Scenario 4: type is a component

// before update:
{ type: Table, props: { rows: rows } }

// after update:
{ type: Table, props: { rows: rows } }

“But nothing had changed!”, you might say, and you will be wrong.

If type is a reference to a function or a class (that is, your regular React component), and we started tree reconciliation process, then React will always try to look inside the component to make sure that the values returned on render did not change (sort of a precaution against side-effects). Rinse and repeat for each component down the tree—yes, with complicated renders that might become expensive too!

Taking care of children

Besides four common scenarios described above, we also need to account for React’s behavior when an element has more than one child. Let’s say we have such an element:

// ...
props: {
  children: [
      { type: 'div' },
      { type: 'span' },
      { type: 'br' }
  ]
},
// ...

And we want to shuffle those children around:

// ...
props: {
  children: [
    { type: 'span' },
    { type: 'div' },
    { type: 'br' }
  ]
},
// ...

What happens then?

If, while “diffing”, React sees any array inside props.children, it starts comparing elements in it with the ones in the array it saw before by looking at them in order: index 0 will be compared to index 0, index 1 to index 1, etc. For each pair, React will apply the set of rules described above. In our case, it sees that div became a span so Scenario 3 will be applied. That is not very efficient: imagine that we have removed the first row from a 1000-row table. React will have to “update” remaining 999 children, as their content will now not be equal if compared to previous representation index-by-index.

Luckily, React has a built-in way to solve this problem. If an element has a key property, elements will be compared by a value of a key, not by index. As long as keys are unique, React will move elements around without removing them from DOM tree and then putting them back (a process known in React as mounting/unmounting).

// ...
props: {
  children: [ // Now React will look on key, not index
    { type: 'div', key: 'div' },
    { type: 'span', key: 'span' },
    { type: 'br', key: 'bt' }
  ]
},
// ...

When state changes

Up until now we only touched the props part of React philosophy but ignored the state. Here is a simple “stateful” component:

class App extends Component {
  state = { counter: 0 }

  increment = () => this.setState({
    counter: this.state.counter + 1,
  })

  render = () => (<button onClick={this.increment}>
    {'Counter: ' + this.state.counter}
  </button>)
}

So, we have a counter key in our state object. A click on a button increments its value and changes button text. But what happens in a DOM when we do that? Which part of it will be recalculated and updated?

Calling this.setState causes a re-render too, but not of the whole page, but only of a component itself and its children. Parents and siblings are spared. That is convenient when we have a large tree, and we want to redraw only a part of it.

Pinning down problems

We have prepared a little demo app so you can see most common problems in the wild before we go about fixing them. You can take a look at its source code here. You will also need React Developer Tools, so make sure you have them installed for your browser.

The first thing we want to take a look at is which elements and when cause Virtual DOM to be updated. Navigate to the React panel in your browser’s Dev Tools and select the “Highlight Updates” checkbox:

React DevTools in Chrome with 'Highlight updates' checkbox selected

React DevTools in Chrome with “Highlight updates” checkbox selected

Now try adding a row to the table. As you can see, a border appears around each element on the page. That means that React is calculating and comparing the whole Virtual DOM tree each time we add a row. Now try to hit a counter button inside a row. You see how Virtual DOM updates on change of state—only the concerned element and its children are affected.

React DevTools hints at where the problem might be, but tells us nothing about details: especially whether the update in question means “diffing” elements or mounting/unmounting them. To find out more, we need to use React’s built-in profiler (note it won’t work in production mode).

Add ?react_perf to any URL of your app and go to “Performance” tab in your Chrome DevTools. Hit the recording button and click around the table. Add some rows, change some counters, then hit “stop”.

React DevTools' "Performance" tab

React DevTools’ “Performance” tab

In the resulting output, we are interested in “User timing”. Zoom onto timeline until you see “React Tree Reconciliation” group and its children. These are all names of our components with [update] or [mount] next to them.

Most of our performance problems fall into one of those two categories.

Either a component (and everything branching from it) is for some reason re-mounted on each update, and we didn’t want it (re-mounting is slow), or we are performing an expensive reconciliation on large branches, even though nothing has changed.

Fixing things: Mounting/Unmounting

Now when we have caught up on some theory about how React makes decisions to update Virtual DOM and figured out how to see what is happening behind the scenes, we are finally ready to fix things! First, let’s deal with mounts/unmounts.

You can get a very noticeable speed improvement if you simply mind the fact that multiple children of any element/component are represented as an array internally.

Consider this:

<div>
  <Message />
  <Table />
  <Footer />
</div>

Inside our Virtual DOM that will be represented as:

// ...
props: {
  children: [
    { type: Message },
    { type: Table },
    { type: Footer }
  ]
}
// ...

We have a simple Message which is a div holding some text (think your garden variety notification) and a huge Table spanning, let’s say, 1000+ rows. They are both children of the enclosing div, so they are placed under props.children of the parent node, and they don’t happen to have a key. And React will not even remind us to assign keys through console warnings, as children are being passed to the parent’s React.createElement as a list of arguments, not an array.

Now our user has dismissed a notification and Message is removed from a tree. Table and Footer are all that’s left.

// ...
props: {
  children: [
    { type: Table },
    { type: Footer }
  ]
}
// ...

How does React see it? It sees it as an array of children changing shape: children[0] held a Message and now it holds Table. There were no keys to compare against, so it compares types, and as they are both references to functions (and different functions), it unmounts the whole Table and mounts it again, rendering all its children: 1000+ rows!

So, you can either add unique keys (but in this particular case using keys is not the best choice) or go for a smarter trick: use short circuit boolean evaluation, which is a feature of JavaScript and many other modern languages. Look:

// Using a boolean trick
<div>
  {isShown && <Message />}
  <Table />
  <Footer />
</div>

Even if the Message goes out of the picture, props.children of a parent div will still hold three elements, children[0] having a value false (a boolean primitive). Remember that true/false, null and undefined are all permitted values of a Virtual DOM object’s type property? We end up with something like that:

// ...
props: {
  children: [
    false, //  isShown && <Message /> evaluates to false
    { type: Table },
    { type: Footer }
  ]
}
// ...

So, Message or not, our indexes will not change, and Table will, of course, still be compared to Table (having references to components as type starts reconciliation anyway), but just comparing Virtual DOM is often a lot faster than removing DOM nodes and creating them from scratch again.

Now let’s look at something more evolved. We know you like HOCs. A higher-order component is a function that takes a component as an argument, does something, and returns a different function:

function withName(SomeComponent) {
  // Computing name, possibly expensive...
  return function(props) {
    return <SomeComponent {...props} name={name} />;
  }
}

That is a very common pattern, but you need to be careful with it. Consider:

class App extends React.Component() {
  render() {
    // Creates a new instance on each render
    const ComponentWithName = withName(SomeComponent);
    return <SomeComponentWithName />;
  }
}

We are creating a HOC inside of a parent’s render method. When we re-render the tree, our Virtual DOM will look like this:

// On first render:
{
  type: ComponentWithName,
  props: {},
}

// On second render:
{
  type: ComponentWithName, // Same name, but different instance
  props: {},
}

Now, React would love to just run a diffing algorithm on ComponentWithName, but as this time the same name references a different instance, triple equals comparison fails and, instead of a reconciliation, a full re-mount has to happen. Note it will also result in a loss of state, as described here. Luckily, it is easy to fix: you need to always create a HOC outside of render:

// Creates a new instance just once
const ComponentWithName = withName(Component);

class App extends React.Component() {
  render() {
    return <ComponentWithName />;
  }
}

Fixing things: Updating

So, now we make sure not to re-mount stuff, unless necessary. However, any change to a component located close to the root of the DOM tree will cause diffing and reconciliation of all its children. With complicated structures that is expensive and can often be avoided.

Would be great to have a way to tell React not to look at a certain branch, as we are confident there were no changes in it.

That way exists, and it involves a method called shouldComponentUpdate which is a part of the component’s lifecycle. This method is called before each call to a component’s render and receives new values of props and state. Then we are free to compare them with our current values and decide whether we should update our component or not (return true or false). If we return false, React will not re-render the component in question and will not look at its children.

Usually to compare both sets of props and state a simple shallow comparison is enough: if we get different values at the top level, we have to update anyway—no need to dig deeper. Shallow comparison is not a feature of JavaScript, but there are numerous utilities for that.

With their help, we can write our code like this:

class TableRow extends React.Component {

  // will return true if new props/state are different from old ones
  shouldComponentUpdate(nextProps, nextState) {
    const { props, state } = this;
    return !shallowequal(props, nextProps)
           && !shallowequal(state, nextState);
  }

  render() { /* ... */ }
}

But you don’t even have to code it yourself, as React has this feature built-in in a class called React.PureComponent. It is similar to React.Component, only shouldComponentUpdate is already implemented for you with a shallow props/state comparison in mind.

It sounds like a no-brainer, just swap Component for PureComponent in the extends part of your class definition and enjoy efficiency. Not so fast, though! Consider these examples:

<Table
    // map returns a new instance of array so shallow comparison will fail
    rows={rows.map(/* ... */)}
    // object literal is always "different" from predecessor
    style={ { color: 'red' } }
    // arrow function is a new unnamed thing in the scope, so there will always be a full diffing
    onUpdate={() => { /* ... */ }}
/>

The code snippet above demonstrates three most common anti‑patterns. Try to avoid them!

If you are mindful of creating all objects, arrays, and functions outside of render definition and making sure they don’t change between calls—you are safe.

You can observe the effect of PureComponent in the updated demo where all table’s Rows are “purified”. If you turn on “Highlight Updates” in React DevTools, you will notice that only the table itself and the new row are being rendered on row insertion, all other rows stay untouched.

However, if you can not wait to go all in on pure components and implement them everywhere in your app—stop yourself. Comparing two sets of props and state is not free and for most basic components is not even worth it: it will take more time to run shallowCompare than the diffing algorithm.

Use this rule of thumb: pure components are good for complicated forms and tables, but they generally slow things down for simpler elements like buttons or icons.

Thank you for reading! Now you are ready to apply these insights in your applications. You can use the repository for our little demo (with and without PureComponent) as a starting point for your experiments. Also, stay tuned for the next part of the series, where we plan to cover Redux and optimizing your data to increase the application’s general performance.

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.

How can we help you?

Martians at a glance
17
years in business

We transform growth-stage startups into unicorns, build developer tools, and create open source products.

If you prefer email, write to us at surrender@evilmartians.com