I recently built a React project that dealt with a large amount of data stored in a Redux store that was consumed by a significant amount of components on the page. I knew this might be a performance concern eventually, but React and Redux seemed to be handling it well enough and this project was not a production UI so I tabled optimization.
However, as the complexity of the project grew and more team members were brought in to begin working on it, its deficiencies began to show. Drag-and-drop UI (courtesy of React Beautiful DND) began to slow down, navigation had noticeable delays, all of this exacerbated on my co-worker’s slightly older machine. So, I began to optimize for performance. Here are some of the key take-aways I found:
Large, single Redux store key
If you’ve got a large Redux stored state key (in my case a ~450 item array of objects with about 20 keys each), it can be incredibly slow when making changes to any part of that stored state for any component using pieces of that data set. Due to the nature of the project, a large store of data like this was necessary, so I had to find some workarounds. To keep modification of this data set as minimal as possible I implemented the following strategies:
- Only add to the state key when an action is completed
- Queue things to be added to the state key in a different Redux state key
- Don’t modify items in the state key until a user action is completed
- Take things that were modifying items in this large state key and put them in a different Redux state key with data information that allowed me to relate items in the new state key to items in the large state key for comparison
Everything gets updated, all the time…
If you’ve got a component that uses any piece of the global redux state and that piece of the global redux state is modified in any way by any other component – even if the data you’re passing to the component via
mapStateToProps you’re dealing with has not changed – it will trigger an update – even though none of your props changed. This cascades down to all of that component’s child components too, so try not to attach a lot of frequently updated properties to your
App component or everything gets updated… a lot…
Component has no update checks
This is sort of a “duh”, but
React.Component has no update checks, you have to do this manually. Unless you say otherwise, the
render() method will be called with any change, even though it may not actually change anything in the DOM. Even though the DOM may not be modified, the processing that leads up to the DOM reconciliation (and the reconciliation itself) done at scale is a big performance hit.
Use PureComponent and Functional components wherever you can
I didn’t want to have to write
shouldComponentUpdate checks on every component, especially since React provides a better convention for handling performance concerns like this. So, I converted everything I could into a
PureComponent or a Functional component. Extending
PureComponent instead of
Component automatically adds a shallow compare of the props and state objects to prevent unnecessary updates. This is great to prevent re-renders. Keep in mind that
PureComponent components will not re-render any of its child components if it doesn’t also re-render.
Functional components have other means of circumventing constantly re-rendering and are also a great way to keep updates to a minimum. Good rules of thumb to follow:
- If you don’t need
defaultPropsettings, make it a Functional component.
- If you don’t have a lot of children components that are heavily dependent on multiple state/prop changes from Redux or their parents, make it a
Keep reducers simple
I was making redux reducers WAY too complicated. I optimized a TON of code in all my reducers after realizing the simple truth that, what’s returned from the reducer is what the new value will be. This resulted in a great simplification of code there and removal of a dependency on lodash’s
cloneDeep (which, while awesome and convenient for deep cloning, is performance costly). For those things I still needed deep cloning, I was able to hack in a little JSON compatible data deep cloning:
var cloned = JSON.parse(JSON.stringify(state))
Keep in mind that trick only works on JSON compatible data (primitives like Strings, Numbers, Booleans, Arrays, and Object literals). Also, it makes me feel a little dirty because it really is a hack, but hey its fast and works for my data set.
React DevTools FTW
Another “duh”, but when performance debugging, the React DevTools extension was a huge help. Specifically in this case, the Highlight Updates checkbox was a great way to reveal how inefficient my code was :-/