React Performance and MobX

Workday Technology
Workday Technology
Published in
7 min readFeb 21, 2018

By Sharon Kong, Software Development Engineer, Workday

One of React’s claims to fame is its fast performance. React maintains a Virtual DOM in memory and only updates the browser’s DOM with differences when they are found. React optimizes for performance, since maintaining the Virtual DOM is generally fast and updating the browser’s DOM is generally slow. However, it’s possible to run into performance issues in a large web application, if you don’t design the application carefully. This article discusses React performance and ways that performance can be optimized in a MobX-React environment.

How can React run into performance issues?

Maintenance of the Virtual DOM can become a performance issue for large applications. In extreme cases, the Virtual DOM might be recalculated for the entire application on every update! For small applications, that may be fine. But for larger applications, that could potentially result in a performance issue.

When will the Virtual DOM be recalculated for a React component?

Every time render() is called on a component, the Virtual DOM is recalculated, and React’s reconciliation process updates the browser’s DOM with any differences that it finds. Calling render() on a component automatically calls render() on all its child components as well. This happens recursively, so calling render() on a component also calls render() on the component’s entire descendant tree.

When is render() initiated in React?

I often see articles that say “render() is called when the state or props have changed”, but this is a deceptive description.

In standalone React, render() is initiated in the following three ways:

  • By setState() — whether or not the new state is equivalent to the old state.
  • By forceUpdate() — for emergency use only.
  • By render() of the parent component — whether or not props have changed.

In all these cases, render() will be called, cascading down the hierarchy, and the Virtual DOM will be recalculated, even if the state and props are the same as before.

Note that React-Redux and MobX-React may shortcut this process (as we will discuss in a moment), so many people may not experience this behavior, or even realize that this is how React works by default.

How can I prevent render() from cascading when state and props have not changed?

React provides an update lifecycle method that can be used to prevent render() from being called on a component, and hence stop the cascade down that branch of the descendant tree. The method is called shouldComponentUpdate(nextProps, nextState). The default implementation simply returns true, allowing the update lifecycle to continue. It can be overridden to return false when appropriate to prevent render() from being called on the component.

Should I implement shouldComponentUpdate() on all my components?

No. Libraries such as React-Redux and MobX-React employ some strategies that prevent the need for implementing a custom shouldComponentUpdate(). Also, executing shouldComponentUpdate() on simple components can be slower than just re-rendering the component. So shouldComponentUpdate() should only be overridden where it shows a measured performance gain.

In addition, you may have heard of React.PureComponent or the legacy PureRenderMixin. They override the default shouldComponentUpdate() with a very efficient comparison for immutable state and props. They simply perform an object reference or primitive value comparison of the existing and incoming state and props and return false if they are equal. This is, in fact, one of the React optimizations that MobX-React employs.

Using the MobX-React @observer decorator automatically overrides shouldComponentUpdate() to use the PureRenderMixin's quick comparison (similar to React.PureComponent). The React-Redux library employs a similar override of shouldComponentUpdate() on components using connect(). Even though MobX is not using immutable state, this scheme still works because MobX-React doesn’t consult shouldComponentUpdate() before initiating render() on a component when the relevant observable values have changed (as we will discuss further in a moment).

In light of this, there are some anti-patterns that you should avoid when passing down props. You should avoid passing down object literals, array literals, or arrow functions that are dynamically created each time render() is called. In these cases, the quick reference check always fails, since the props always point to a new object reference. For example:

Bad:

class MyComponent extends React.Component {
// Shouldn’t dynamically create objects when passing down props
render() {
return (
<MyWidget
obj={{ foo: ‘bar’ }}
array={[]}
onClick={() => { alert(‘hi’) }}
/>
)
}
}

Good:

class MyComponent extends React.Component {
// Objects passed down as props are
// instantiated outside of render()
myObj = { foo: ‘bar’ }
DEFAULT_ARRAY = []
handleClick = () => alert('hi')
render() {
return (
<MyWidget
obj={this.myObj}
array={this.DEFAULT_ARRAY}
onClick={this.handleClick}
/>
)
}
}

When does MobX-React initiate render() on a component?

When React components are defined with the MobX-React @observer decorator, their render() method is automatically called when a dereferenced observable value is changed. This means that only relevant observables will trigger a render(), and that render() is triggered at exactly the point in the hierarchy where the observable is used. Thanks to MobX-React’s automatic shouldComponentUpdate() override, the re-rendering only cascades as far down as necessary.

The following examples demonstrate these points while using MobX-React to replace component state. Yes, it’s possible to use MobX with React and never make any setState() calls at all! Of course you can still use setState() if you want to, but our team currently doesn’t. By exclusively using MobX, we not only get some performance gains, but we also avoid some of the confusing parts of setState(), such as the fact that it is asynchronous and may batch state updates.

In standalone React, any call to setState() triggers render(), whether or not the new state is the same or is irrelevant to the render() method. In the following MobX-React example, only str2 is used in render() of Parent, so only a change to str2 triggers render(). Changes to str1 or str3 don’t trigger render(), as they’re not relevant to the render() method.

@observer
class Parent extends React.Component {
@observable str1 = ‘1’
@observable str2 = ‘2’ // Only changing str2 causes render()
@observable str3 = ‘3’
render() {
return (
<Child str={this.str2} />
)
}
}

It’s also possible to pass down objects with observable properties as props to child components. In these cases, render() is automatically called on the components where the observable property is dereferenced, meaning where the observable property value is used. In the following example, the observable object obj has an observable property str. The object is then passed from Parent component to Child component as a prop. When obj.str is changed, only Child re-renders since its render() dereferences the observable property str. Parent doesn’t re-render in this case, as it only passes the observable object along, without using the str observable property’s value.

class Parent extends React.Component {
@observable obj = { str: ‘1’ }
// Parent’s render() will not be called when obj.str is changed
render() {
return (
<Child obj={this.obj} />
)
}
}
@observer
class Child extends React.Component {
// Only Child’s render() will be called when obj.str is changed
render() {
const { obj } = this.props
return (
<span>{obj.str}</span>
)
}
}

This leads to the recommendation that observable properties should be dereferenced as late as possible in the component hierarchy. It’s more performant to pass down observable objects as props, as opposed to passing down their property values. However, this can lead to less specific APIs, so this approach should only be used to the extent that it makes sense. In the following examples, the observable object person has an observable property name. In the first example, the observable person object is passed from MyComponent to the DisplayName component as a prop. When person.name is changed, only DisplayName re-renders. In the second example, the person.name property is dereferenced and passed to the DisplayName component as a prop. When person.name is changed, MyComponent re-renders, which in turn causes all its children to re-render, including the DisplayName component.

More performant:

class MyComponent extends React.Component {
// MyComponent render() will not be called
// when person.name changes
render() {
return (
<div>
<DisplayName person={person} />
...
</div>
)
}
}

Less performant, but a more specific API:

class MyComponent extends React.Component {
// MyComponent render() will be called when person.name changes
render() {
return (
<div>
<DisplayName name={person.name} />
...
</div>
)
}
}

(There is a compromise by creating a PersonNameDisplayer using a stateless component function. Our team uses Typescript with strongly-typed functions, which makes using this pattern extremely unwieldy.)

What are some React best practices that still apply to the MobX-React environment?

In general, it’s a good idea to structure your components so that they are isolated, focused, and decoupled. It is especially good to isolate large groups of related dynamically generated content into their own component, so that a single shouldComponentUpdate() can appropriately manage the re-rendering for the isolated group.

In the following example, React will needlessly call render() on all the TodoItem components when the user.name changes. They won’t actually re-render to the DOM in this case, but the reconciliation process in itself can be expensive, if there are a large number of dynamically generated components.

Bad:

@observer 
class MyComponent extends React.Component {
// Couples the rendering of user.name and the
// dynamically generated TodoItems list
render() {
const {todos, user} = this.props
return (
<div>
{user.name}
<ul>
{todos.map((todo) => {
return <TodoItem todo={todo} key={todo.id} />)
})}
</ul>
</div>
)
}
}

Good:

@observer
class MyComponent extends React.Component {
render() {
const {todos, user} = this.props
return (
<div>
{user.name}
// todos are rendered by a separate component
<TodosView todos={todos} />
</div>
)
}
}
@observer
class TodosView extends React.Component {
// shouldComponentUpdate() can stop render() from being called
// if the list of todos has not changed
render() {
const {todos} = this.props
return (
<ul>
{todos.map(todo => <TodoView todo={todo} key={todo.id} />)}
</ul>
)
}
}

In closing

We have discussed some ways in which React performance can be optimized in a MobX-React application. However, I do want to mention that it’s a best practice to do performance tuning only if you are actually running into performance issues. When that is the case, profiling should be done to see exactly where the problem areas are. Then changes to those problem areas should be made, ideally one-by-one, and the profiling should be re-done to ensure that performance was actually improved. Performance tuning is tricky business… Good luck!

Resources

--

--