The Virtual DOM and its Anti-Patterns

SOUND FOREST by WILLPOWER STUDIOS (WILLIAM ISMAEL, CC BY-ND 2.0)

Riipen chose React with Redux as a front-end framework for its modern approach to development and performant architecture. As our web application has grown, we’ve discovered a few ways that seemingly innocuous code in React components can slow the browser to a crawl. As is often the case with optimizations, the same design decisions that make React speedy can also cause it to be sluggish if you’re not mindful of a few anti-patterns.

In Riipen’s case, we also use Redux Form, which updates the state on every input event triggered by the user (like a key press). In some cases, this caused components in our application — completely unrelated to a form — to be re-rendered every time the user typed a letter! For this reason, it’s essential to us that every component be as performant as possible.

In this three-part series, we’ll explore common causes of slow components and wasteful rendering. The first part will take a deeper look at why React can be fast in the first place, and how we can help it stay that way in practice. In the second post, we’ll look at some other causes of performance issues, particularly in Redux applications, and ways to avoid unnecessary computation before React gets involved. And in the final part, we’ll look at some performance tools that you can use to find good optimization candidates in your existing code bases.

Most of the information should generalize across all React apps, but our optimization tips are written with Redux in mind.


Why is React fast?

React has a reputation for being fast mostly because of one powerful feature: the virtual Document Object Model (DOM). It allows React to skip updating the browser’s DOM when rendering a component if the content of that component hasn’t actually changed. Changing the actual DOM is a costly operation, because doing so often forces the browser to recompute styles and render new HTML elements for the user, even if they are essentially identical to what was previously on the page.

Equipped with a virtual DOM, React performs a much simpler operation called “reconciliation.” By comparing the new element tree returned from a component’s render function with what is currently rendered on the page, React can avoid unnecessary updates and only change the page if the tree is different.

See Gethyl George Kurian’s post about reconciliation (virtual DOM diffing) for a more in-depth exploration of that process.

With this extra bit of effort, React can save the browser a lot of pointless work. If reconciliation determines nothing has changed, React effectively skips everything that would usually happen after it calls a component’s render method. In some cases, simply bypassing the browser’s render cycle yields an enviable performance boost.

But React needs your help to do this most effectively. It’s pretty easy to spoil the benefit of reconciliation by creating components that always return a different element tree — even if their presentation and behaviour never changes.

Handling events

If you are creating event handlers via closures in your render function like so:

render() {
const logToConsole = () => {
console.log(‘Debug clicked’);
};
  return (
<Link onclick={logToConsole}>Debug</Link>
);
}

logToConsole will reference a new function on every render and React won’t be able to tell them apart. As a result, the browser DOM may be updated on every render, even though the content and behaviour of the component isn’t changing.

Instead, help out React by creating the function as a method on the class (or using a non-local function declared in a higher scope):

logToConsole() {
console.log(‘Debug clicked’);
}
render() {
return (
<Link onclick={this.logToConsole}>Debug<Link>
);
}

Now, every time render is called, the exact same function will be assigned to the onclick property of the <Link> component, making it easy for React to tell that the component hasn’t changed between renders.

If you need to access this inside your event handler, create the method using an ES6 arrow function:

logToConsole = () => {
console.log(`Debug clicked for ${this.props.user.name}`);
}

References to objects and arrays

As Esa-Matti Suuronen explains, creating a new object or array in render can cause the same issues, because a new reference is created every time the method is called. Consider:

render() {
const adminActions = this.props.isAdmin ? [‘edit’, ‘delete’] : [];

return <Toolbar actions={adminActions} />;
}

This code seems fairly innocuous at first glance. But, in every case, the render method will pass to the Toolbar component a new reference to a new array, even if the array is empty. In JavaScript, [] !== []. Unless React elects to execute a shallow diff of the actions Toolbar property, it will determine the property has changed on every render.

The solution is to declare constants in a higher scope:

import React from 'react';
const ADMIN_ACTIONS = [‘edit’, ‘delete’];
const EMPTY_ARRAY = [];
class Component extends React.Component {
render() {
const adminActions = this.props.isAdmin ?
ADMIN_ACTIONS : EMPTY_ARRAY;
    return <Toolbar actions={adminActions} />;
}
}

or to memoize the output of a function or method using a function like lodash.memoize:

import memoize from 'lodash/memoize';
import React from 'react';
class Component extends React.Component {
constructor() {
super();

this.getActions = memoize(this.getActions);
}
  getActions(isAdmin) {
return isAdmin ? [‘edit’, ‘delete’] : [];
}
  render() {
return <Toolbar actions={this.getActions(this.props.isAdmin)}>
}
}

The same goes for object literals ({}) and every other construct for which x !== x.

Watch out for closures — especially in map and forEach loops — which may be creating new references to the same data every time they are executed.

The difference may seem trivial, and it’s easy to slip new references into a changeset accidentally; using an empty Array here or there is generally good JavaScript. But rendering a new identity in the wrong place — particularly frequently used components — can cause a measurable performance decrease in your web app.


Next time, we’ll look at some optimizations specific to Redux, and ways to skip rendering and React’s reconciliation process altogether.

Stay Riipe!