One of the main benefits of using a framework like React is the virtual DOM. By using a virtual DOM React is able to only render the DOM elements that actually changed. The DOM elements that have not changed do not rerender. For many websites, the slowest action is the rendering of DOM elements. (A DOM element is just an HTML element, like a image, a field, a div, etc.)
This is great for simple applications and websites, but for more complex applications, unintended code can for React to rerender components. This is especially true when adding Redux to a React application.
Luckly, even if the render function of a component runs, that does not mean the the DOM element is going to be rerendered. React is smart enough to only change DOM elements when needed. However, running the render function needlessly can still have a large negative effect on the speed of your application.
You can see how often your React components are rendered by using React Dev Tools. Install the React Dev Tools plugin, open your Chrome web developer console, click on the ‘React’ tab, check the ‘Highlight Updates’ checkbox. Now when you interact with your React application you will see when components rerender. The rerendered components are highlighted with a blue line. That blue outline turns yellow or red if the component is being rerendered multiple times. This is bad. A component should not be rendering multiple times per second or per user action. You can likely speed up your application if you figure out why the rerendering is happening and prevent it.
I recently spent a couple of days investigating the sluggishness of a complex React Redux application. The site was starting to act slow enough that users were noticing. I learned a lot about optimizing React and preventing unwanted rerenders of components. I was able to refactor some of the code in the applications and now the website is faster than ever. Here are a few things you should look out for when trying to optimize the performance of a React application.
Too Many Computations In mapStateToProps
Putting too much expensive code in mapStateToProps was the biggest factor of my application being slow. This actually did directly have anything to do with components rerendering. The mapStateToProps function is run before a component renders. It is run anytime a prop is updated to the Redux store is updated. So any change to the Redux store will trigger the mapStateToProps function in all the components that subscribe to the store. For a complex application, this can be many different components.
To be perfectly clear, any change to the store will trigger all mapStateToProps functions. This includes changes that are completely unrelated to the component you may be investigating.
Because this code runs before a component is rerendered, you do not see the effects of this function by using the React Dev Tools plugin.
The important thing here is to remember that mapStateToProps is run all the time. So if you are working with a large amount of data, iterating through all that data in the mapStateToProps is a bad idea.
Avoid calling any expensive calculations in the mapStateToProps function. If possible, move these calculations to other parts of your application.
If you are transforming one type of data to another type of data using something like Array.map in mapStateToProps, consider expanding your Redux store to have both the original data and the transformed data stored in the store. This will take up a bit more memory but will save on an expensive computation being run anytime any (potentially unrelated) changes are made to the Redux store.
Another place to move these expensive calculations is to the render function. The function will still be run, but much less often. The render function is only ran when the props the component receives change.
Creating New Objects In mapStateToProps
React components rerender when they get new props. If the props don’t change, then the component does not rerender.
mapStateToProps returns an object that is passed into a component as a series of props. If any of these values change, the component will rerender. Take a look at the following:
function mapStateToProps(state) {
return {
aRandomProp: Math.random()
}
}
The component where this lives now can use this.props.aRandomProp which is a random number between 0 and 1. The problem here is that anytime mapStateToProps is run, aRandomProp gets a new value. Anytime any prop gets a new value the component needs to rerender. So what this is doing is forcing the component to run its render function anytime mapStateToProps runs. And mapStateToProps runs anytime the Redux store changes, which with complex applications is very often.
This will lead to a ton of rerendering.
So the above example is super contrived. However, the following is a lot more likely:
function mapStateToProps(state) {
return {
AFilteredArray: state.anArray.filter(value => value.isTrue === true)
}
}
Here we have a component that wants to look at the Redux store’s anArray property. However, the component only wants to look at the values of anArray that have a property isTrue set to true. The filter method is a nice way of filtering an array and only getting the elements that you care about.
The problem here is that the filter function returns a new array.
React uses a shallow comparison to see if a prop’s new value is equal to its old value. Something like newProp === oldProp. In JavaScript, the equality of an array and object is done by checking to see if their value is a reference to the same object. In JavaScript [] === [] is false. Those two arrays contain the same elements (empty in this case) but they do not have the same reference.
To expand:
a = [];
b = [];
a === b; // false
c = [];
d = c;
c === d; // true
So the point I am trying to make is:
a = [];
b = a.filter(() => true); // b is a new array
a === b; // false
And so using a function like filter in mapStateToProps will create what looks like a brand new value to React, forcing the component to rerender.
The solution here is, don’t create new objects or arrays in mapStateToProps. Remember, primitives like strings are compared by value, objects are compared by reference.
Creating New Objects In The Props
A component will rerender if it gets new props. These props can come from the Redux store is the component subscribes to the store using mapStateToProps, but it will also rerender if its parent component passes down new props. This means that you should not create new arrays or objects unnecessarily when passing down props between components. For more detail, see the section above.
Defining Functions In The Render Function
The render function runs whenever new props are passed to a component. Don’t add unnecessary computations into the render function. One common mistake is to define functions that are only used in the render function within the render function.
render () {
const aFunction = () => true; // created every time render runs
...
In the above aFunction has to be recreated every time render runs. This is probably not what you want. Instead, define that function somewhere else and just call if where needed inside the render function.