#React optimizations
The main reason why our UI felt sluggish was because we were rendering components unnecessarily. When one component in the UI was interacted with, many unrelated components would be re-rendered because application state was “touched” and many derived props were re-computed.
An easy way to improve this situation is to use React’s built-in PureComponent instead of plain Component. PureComponent implements the the lifecycle method shouldComponentUpdate, in which it performs a shallow comparison of props and state to decide if a re-render is necessary.
However, in many cases in our application properties in props are derived from the Redux state. This means that the properties might be different by reference, even though they have the same value. Luckily we can remedy this by doing a deep comparison in shouldComponentUpdate.
shouldComponentUpdate(nextProps: Props, nextState: State) {
return !R.equals(this.props, nextProps) || !R.equals(this.state, nextState);
}
Here we are using Ramda’s equals to compare state and props.
shouldComponentUpdate is also useful when you want to have special handling for some properties. For example, for some of our components it doesn’t make much sense to re-render during scrolling even if other props have changed:
shouldComponentUpdate(nextProps: Props, nextState: State) {
return !nextProps.isScrolling &&
(!R.equals(this.props, nextProps) || !R.equals(this.state, nextState));
}
Disclaimer: It’s important to note that this isn’t a one-size-fits-all solution. Deeply comparing objects can be expensive and if the property actually changed then you just end up adding additional computation time while achieving nothing.
In many cases in our application we have callbacks which dispatch multiple actions. It’s often not necessary for the effects of each action to happen individually, and even harmful when this results in the UI showing in an inconsistent state. We can save a lot of effort by suspending store notifications until all actions are dispatched. If you’re interested, see here for how we enhance the Redux store to handle suspending notifications.
Redux’s connect() function supports it’s own version of shouldComponentUpdate. Namely, it has functions areStatesEqual, areOwnPropsEqual, areStatePropsEqual and areMergedPropsEqual which can be passed in as options. By providing these functions (and making them perform a deep-ish comparison) we can stop unnecessary re-renders before the Component level.
export default connect(mapStateToProps, mapDispatchToProps, undefined, {
areOwnPropsEqual: R.equals,
areStatePropsEqual: R.equals,
})(View);
It’s a good idea to use a library like reselect with Redux to reduce computational load when deriving data from the app state. There are few tricks to make it even better.
createSelector, by default, compares by value. This gives a lot of false positives in any real-world derived data graph. You can define your own createSelector using createSelectorCreator and passing custom equalityCheck function to it but, as we’ve seen with shouldComponentUpdate, doing full-depth comparison by value is expensive. We use a 2 level deep comparison with simplified object handling (not sorting keys).
Write your selectors carefully
Often times performance degradations happen when selectors are inefficient. There are a few problems that we ran into that were dramatically slowing down our application
Selectors that aren’t specific enough. Selectors should not depend on data that they do not use.
Using a uniq function which used a deep comparison to determine unique values in an array. In most cases we don’t need comparison by value, and Array.from(new Set(…)) is enough.
Returning items from selectors that are opaque to comparison (i.e. functions).