3 small tips for better Redux performance in a React app
In my previous article I went through some core concepts of performance optimisation in React apps. This article will extend these concepts by focusing on practices which will make sure that the introduction of Redux into your project is not causing any perf bottlenecks as your app grows.
Redux itself is simply an event emitter as Andrew funnily pointed out. Thus, all possible optimisations are automatically delegated to React itself. In React, we translate optimisations into rendering time reduction and reduction of overall renders. The former has to do with the amount of work that is performed during the render phase of a component (expensive computations, multiple function invocations, custom DOM mutations, etc.), while the latter with the number of times that a component has rendered. The amount of work inside a React component is strongly tied to the business logic and is most likely unrelated to redux, but the number of renders can easily be relevant to redux and can gradually increase if you don’t take care.
So, here we go:
1. Make use of memoization in your selectors
Selectors are nothing new in redux, they are simply a way of abstracting the actual state implementation from the component that’s reading it; a proxy between your component and the actual redux state. Writing a selector can be as easy as this:
In the above example, we assumed that the data were stored in a list. Redux docs though, recommend that state is normalized. That means that we should store the items in an object where the keys are the IDs of the items and the values the items themselves. In order to achieve a similar result as the example above, we would then naively rewrite a selector like that (assuming that order of items doesn’t matter):
// remember, items is an object in this case
export const getItems = state => Object.keys(state.items);
This implementation actually serves our purpose, but has one big flaw. Any time any piece of the Redux state is updated, our Component will re-render, regardless of whether the store update was related to our component or not. You see, when you connect a component using react-redux’s connect()
HOC, Redux re-calculates the output of the mapStateToProps
that has been supplied to the connect()
and performs a shallow equality check. If the props before and after the store update are referentially the same, then the component does not re-render. If they are different though, then the component will re-render.
In our case, Object.keys
returns a different instance every time it is invoked. Thus, since this selector is within our mapStateToProps
, it will get re-evaluated every time a store update is performed. Just because the reference of this array will be different than its previous one, react-redux will falsely think that the props of the component are different and so it will “allow” React to re-render.
Operators like .map
and .filter
return referentially different values every time they are invoked. Thus, we must make sure that when the actual output is the same, then the reference stays the same and when the actual output is different, then the reference should be different as well. To do that, we employ a technique called memoization. This technique guarantees that “as long as the inputs to a function are the same, then the output will be referentially the same”. One popular library option to implement that is reselect. With it, we could re-write our selector like so:
import { createSelector } from 'reselect';export const getItems =
createSelector(state => state.items, items => Object.keys(items))
The createSelector
accepts an arbitrary number of function arguments, where the first N-1 functions are “inputs” to the selector and the Nth one is the “output”. It guarantees that as long as the inputs have the same value, the output will be “cached” and re-used. If any of the inputs has a different value compared to the value it had during the last selector invocation, then the “output” from the Nth function will be re-computed. In our example, as long as the items remain the same, then the getItems
selector will return a value with the same reference, no matter how many times it’s called. If new items are added, then the items
state won’t have the same value anymore, so the getItems
will return a new reference which will be cached and re-used.
As a rule, anytime you need to perform an operation that returns a new reference on every selector invocation, make sure you memoize the result (Tip: you can use a memoization selector inside another memoization selector when dealing with complicated selector logic).
2. Use referentially consistent action creators
Sometimes action creators are simple functions like so:
const fireAction = () => ({ type: 'ACTION', payload: {} });
In the above case, fireAction
has a fixed reference, but unfortunately that’s not always the case. There are times where you need to pass the component’s props
to your action creators in order to differentiate the data passed to your reducer. For example, let’s take a look at the following case:
The above implementation is a bit naive. We want to “bind” the fireAction
action creator to the particular item
that MyComponent
has as prop. To achieve that, we “read” the props from within mapDispatchToProps
and use them to create fireActionWithItem
. What some people don’t know, is that whenever you use the component’s props from your mapDispatchToProps
(whenever you use the 2nd argument of this function), react-redux will recalculate the output of mapDispatchToProps
each time the component’s props change. Thus, if any prop from MyComponent
were to change, then mapDispatchToProps
would run again. Because of the nature of anonymous functions, the fireActionWithItem
that is passed as a prop to MyComponent
would be referentially different every time . That’s not a problem for the component that is rendering (since it would re-render regardless of what happened inside our mapDispatchToProps
), but it might create issues for other components declared inside MyComponent
that get fireActionWithItem
as prop. For example, let’s take a look at the following scenario:
We have 2 components: MyComponent
and ExpensiveComponent
. As you can see, ExpensiveComponent
is wrapped in a memo()
so it will only re-render when its props change. Each time the randomProp
in MyComponent
changes, then the mapDispatchToProps
will be re-calculated, the fireActionWithItem
will get a new reference, MyComponent
will re-render and ExpensiveComponent
will be forced to re-render as well since its onClick
prop will be different. Ultimately, ExpensiveComponent
shouldn’t have re-rendered at all since nothing that affects it has actually changed.
To combat scenarios like that, you can either delegate the firing of the action with the correct params to the component itself (by passing both item
and fireAction
as props to the component), allowing it to “create” the fireActionWithItem
with a fixed reference (by making use of .bind
& useCallback
), or you can use memoization techniques in your action creators. For the latter, you can employ the same concepts as the ones you employed in selectors and make sure that the reference is always the same. Both options can be seen in the gist below:
3. Batch actions to reduce # of renders
Whenever you have to chain multiple actions one after the other (which you intentionally keep separate for separation-of-concerns in your actions) , you would be better off batching them and firing them with a single dispatch
. Grouping these actions can guarantee that instead of X potential re-renders, there will only be 1. There is a caveat though; in order for the actions to be “batch-able”, they need to be independent of one another. That means that the N action must be independent of the state created by the N-1 action. In other words, you should be able to change the firing order of these actions without affecting the end state. If that’s the case, react-redux
7.x.x offers a new batch
function that helps you update your state with only a single dispatch, saving you the additional — potentially costly — renders.
import { batch } from 'react-redux'
import { action1, action2 } from './actions';// dispatches both actions at the same time
batch(() => {
dispatch(action1());
dispatch(action2());
});
In order to use it would need access to the dispatch function. There are lots of ways to achieve that, depending on the where the actions are fired from. For example, you can get access to dispatch
from your mapDispatchToProps
function, from a redux-thunk
thunk or from manually importing the store
and using store.dispatch
. It mainly boils down to how your app is structured. It has to be said that this technique is rarely used, but can yield performance gains in cases where freedom of the main thread is crucial. If, for example, you were performing heavy animations after firing these actions, then a single dispatch might save you precious frames.
Conclusion
I intentionally haven’t touched the topic of correctly organising your state which is even more crucial than the above tips. The purpose of this article was to shed light to potential performance losses through non-optimal integration of actions-creators & selectors in a React app. Under no means do I believe that these concepts should be used by all projects — since for most of them they are an overkill — but they are good to know.
Thanks :)
P.S. 👋 Hi, I’m Aggelos! If you liked this, consider following me on twitter and sharing the story with your developer friends 😀