Blog

React + Redux: our pursuit of performance (part 1)

While having dabbled with the critically acclaimed React + Redux combo for the past year, we've had our fair share of performance challenges. While React is pretty darn fast out of the box, there comes a time that you'll run into a brick wall, performance wise. This is a series of posts for those who are in the same boat as we were and will serve as a guide to row this boat safely to shore. We will be sharing our experiences and providing solutions.

Improving performance starts with measuring the current performance. Then apply tweaks and measure again to see if the performance actually improves. This is where React Performance Tools come in handy. It gives you insight in how long it takes to mount & render React components, and even show you 'wasted' time, where component renders stayed the same, so the DOM wasn't touched. There's even a Chrome Extension! We'll be using the latter since it's simple to setup and use.

In this post we'll show you the steps we've taken and the lessons we've learned profiling a React app and improving it's performance. An example app to help visualize the challenges/solutions we cover in this post is available on Github.

A basic example

Consider the following example. An app that displays a list of buttons that highlight when activated.

index.js

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import reducer from './reducer'
import App from './App';

const store = createStore(reducer);

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.js

import React, { Component } from 'react';
import { connect } from 'react-redux';

import { turnOn, turnOff } from './reducer'
import Button from './Button';

const range = [...Array(5).keys()];

class App extends Component {
  handleButtonClick(index) {
    if (this.isButtonActive(index)) {
      this.props.dispatch(turnOff(index));
    } else {
      this.props.dispatch(turnOn(index));
    }
  }

  isButtonActive(index) {
    return this.props.activeIndices.indexOf(index) !== -1;
  }

  render() {
    return (
      <div>
        {
          range.map(index => {
            return (
              <Button
                key={index}
                index={index}
                onClick={this.handleButtonClick.bind(this)}
                active={this.isButtonActive(index)}
              />
            );
          })
        }
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    activeIndices: state
  };
}

export default connect(mapStateToProps)(App);

Button.js

export default class Button extends PureComponent {
  render() {
    const { active, index, onClick } = this.props;

    return (
      <button className={active ? 'active' : ''} onClick={() => onClick(index)}>click me</button>
    );
  }
}

reducer.js

const initialState = [];

export default (state = initialState, action = {}) => {
  switch (action.type) {
    case 'ON':
      return state.concat(action.itemIndex);
    case 'OFF':
      const newState = state.slice();
      const index = newState.indexOf(action.itemIndex);
      newState.splice(index, 1);
      return newState;
    default:
      return state;
  }
}

export const turnOn = (index) => {
  return { type: 'ON', itemIndex: index };
}

export const turnOff = (index) => {
  return { type: 'OFF', itemIndex: index };
}

Measure, measure, measure

Install React Perf Chrome extension and add the following to make it work.

App.js (with React Perf)

import React, { Component } from 'react';
...
import Perf from 'react-addons-perf';

export default class App extends Component {
  componentDidMount() {
    window.Perf = Perf;
  }
  ...

}

...

You should now be able to measure your React app's performance using the React Perf extension. It's available in Chrome's DevTools. Let's see what it displays for our example app after we click one of our buttons:

React Perf

At this point you've probably noticed that we have some wasted renders in our app. Let's see what we can do about this.

Meet shouldComponentUpdate()

The shouldComponentUpdate() lifecycle method allows you to tell your component when to re-render. By default it returns true, meaning it will always re-render a component when (new) props are received. Let's think for a moment which prop changes we should consider to determine when to re-render one of our buttons. A simple implementation could be:

export default class Button extends Component {
  shouldComponentUpdate(prevProps) {
    return prevProps.active !== this.props.active;
  }

  ...
}

Let's see what React Perf shows us:

React Perf

No more wasted Button renders! That's what we wanted to achieve. Or is it?

While 'shouldComponentUpdate' could result in a performance boost, please be careful with implementing it, as slow implementations may have even worse performance impacts than a fast wasted render.

Besides, you need to carefully think about which components should implement this method (if at all) to get the best results. Especially given the fact that child components won't get updated as well.

Lastly, implementing shouldComponentUpdate in many different components would become cumbersome and difficult to maintain. So let's look at some alternatives.

Meet React.PureComponent

Thus-far our components have extended React.Component. React offers a different class to inherit from: React.PureComponent. To quote it's documentation:

React.PureComponent is exactly like React.Component but implements shouldComponentUpdate() with a shallow prop and state comparison.

Let's remove our custom shouldComponentUpdate and extend React.PureComponent.

import React, { PureComponent } from 'react';

export default class Button extends PureComponent {
  ...
}

And let's see what React Perf tells us:

React Perf

Darn it! Our wasted renders are back. Lets see what our <Button /> props contain:

  • index
  • active
  • onClick

The onClick function prop is the culprit here. It's passed to the <Button /> component like so:

<Button
  key={index}
  index={index}
  onClick={this.handleButtonClick.bind(this)}
  active={this.isButtonActive(index)}
/>

Now let's see what the documentation of the Function.prototype.bind() function states:

The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

Aha! A new function is created. Every time our <Button /> is rendered. That newly created function is not equal (!==) to the function created in the previous render, resulting in a wasted render.

Luckily there are several ways of setting the 'this' context correctly.

Meet React.createClass

Although React.createClass is deprecated, it's worth mentioning for the sake of familiarity. It will automatically bind this to all functions, but you shouldn't use it.

const App = React.createClass({
  handleButtonClick() {
    // correct 'this' context
  },

  render() {
    return <Button onClick={this.handleButtonClick}>Submit</Button>;
  }
}

Use constructor binding

Another way to set this correctly is by moving the .bind(this) into the class' constructor. This way the function is only recreated once.

class App extends Component {
  constructor(props) {
    super(props);
    this.handleButtonClick = this.handleButtonClick.bind(this);
  }

  handleButtonClick() {
    // correct 'this' context
  }

  render() {
    return (
      ...
        <Button onClick={this.handleButtonClick}>Submit</Button>
      ...
    );
  }
}

Meet @autobind

This is the solution we chose. This autobind decorator will take care of the this binding for you. Without the hassle of having to bind them in the constructor or having to resort to the old React.createClass syntax.

class App extends Component {
  @autobind
  handleButtonClick() {
    // correct 'this' context
  }

  render() {
    return (
      ...
        <Button onClick={this.handleButtonClick}>Submit</Button>
      ...
    );
  }
}

All of these 3 solutions fix our wasted renders.

React Perf

Final thoughts

Building web apps with React has been a pleasant experience for us thus-far. But as with many technologies there are common pitfalls that can slow down your development as well as your apps performance significantly. We've shown you how you can measure your apps performance and gave you some tools to eliminate wasted renders.

This is not where our journey ends. We've come along several other challenges and will discuss them in future blog posts.

Zilverline gebruikt cookies om content en advertenties te personaliseren en om ons websiteverkeer te analyseren. Ook delen we informatie over uw gebruik van onze site met onze partners voor adverteren en analyse. Deze partners kunnen deze gegevens combineren met andere informatie die u aan ze heeft verstrekt of die ze hebben verzameld op basis van uw gebruik van hun services.

Okee