React Performance — Being Proactive vs. Reactive

At some point, most React developers transition from building simple components to developing large, complex applications with lots of nested components. This often brings about the realization that components are re-rendering unnecessarily on every user interaction. Every re-render of a component takes time. These milliseconds add up and can lead to lag times for the user interacting with the page.

sCU to the rescue!

The solution is to make use of React’s lifecycle methods, the most important of which — in terms of performance — is shouldComponentUpdate (sCU). This method takes the arguments nextProps and nextState, which you can use to compare against this.props and this.state to determine if there was a change, and if that change warrants an update to the component. sCU must always return a boolean:

shouldComponentUpdate = (nextProps, nextState) => {
return (true|false)
}

Behind the scenes, React implements sCU for every component which always return true. It’s up to the developer to override this method and define the conditions for updating (re-rendering). A useful technique to enforce strict rendering rules is to initially write the component with sCU return false. By doing this, we're telling the component not to re-render under any circumstances. In the example below, we have a simple component which renders a Button.

class MyComponent extends React.Component {
  shouldComponentUpdate = (nextProps, nextState) => false
  render() {
<Button label='Click me' />
}
}

The long game…

Over time, as the component gains new props or local state, we can unlock the rules for re-rendering as needed. Expanding on our example above, let’s add a loading state, which controls the display of a component to indicate that data is loading.

class MyComponent extends React.Component {
  state = {
loading: false
}
  loadData = () => {
this.setState({ loading: true })
    api.loadData.then(() => {
this.setState({ loading: false })
})
.catch(() {
this.setState({ loading: false })
})
}
  shouldComponentUpdate = (nextProps, nextState) => {
return nextState.loading !== this.state.loading
}
  render() {
<div>
<Button label='Click me' />
<Loader display={this.state.loading} />
</div>
}
}

We’ve now added a condition to sCU which enforces a strict rule for updating; if that condition is not met, the component will not re-render under any circumstances after the initial load. In an application, the Button would probably be contained within a parent. If and when that parent re-renders, we likely don’t need the Button to re-render along with it. We’ve now gained a performance boost!

The reason for adding sCU right off the bat is twofold. One, we have a placeholder for other developers on our team to add conditions as necessary, thereby “unlocking” the component’s rules for updating. In fact, they’ll be forced to if they want the component to re-render after new props or state are added. Two, we’ve approached building our components with a “performance first” mindset. Over the lifetime of the component’s development we’ve ensured that each added piece of complexity will not take away from the user experience. The alternative is waiting until the component becomes complex, at which point we are often forced into addressing performance issues. Figuring out the conditions for re-rendering is a lot harder if you have to do it all at once rather than incrementally over time.

TL;DR

When developing a new component, keep these 3 simple steps in mind:

  1. Always create a sCU lifecycle method.
  2. Adopt a “performance-first” mindset and make sCU return false in all cases.
  3. As the component gains new props and/or state, “unlock” the re-rendering by adding logic to sCU.
Locking down sCU = performance + peace of mind

PS. Join us at the Embassy, we’re hiring!