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:
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:
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:
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.
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.