React Redux: performance considerations when dispatching multiple actions
In a React Redux app, what happens when you dispatch multiple actions in a row?
When used out of the box without any performance optimisations, there are two primary concerns:
- React will re-render multiple times
- React Redux will re-evaluate selectors multiple times
There are well known and documented solutions to the “multiple React re-renders” problem, but the other problem is not so well known. In this article I’ll demonstrate both problems along with their respective solutions.
React re-renders multiple times
By default, React will re-render once for each dispatched action, including for actions that are dispatched together in the same tick of the event loop (except for actions dispatched inside event handlers).
As an example, here is a React app that fetches some data when a button is clicked, and then dispatches 2 actions:
(Full example can be found on GitHub and StackBlitz.)
When we profile this, we can see that React is rendering twice (once for each dispatched action):
Fortunately there is a well established solution to this: the
… starting in React-Redux v7 a new
batchpublic API is available to help minimize the number of React re-renders when dispatching actions outside of React event handlers. It wraps React's
unstable_batchedUpdate()API, allows any React updates in an event loop tick to be batched together into a single render pass. React already uses this internally for its own event handler callbacks. This API is actually part of the renderer packages like ReactDOM and React Native, not the React core itself.
When we profile this again, we can see that React is now rendering only once:
React Redux re-evaluates selectors multiple times
batch function only solves half of the problem:
each dispatched action would still result in a separate notification of subscribers (and running of
mapStatefunctions), but the rendering would get combined.
Each time an action is dispatched, every
connected component will be notified, and as a result all of the selectors used by those
connected components must be re-evaluated. To say that another way: each time an action is dispatched, every selector in the app must be re-evaluated.
When we’re dispatching multiple actions in a row, this can get quite expensive. We only want to re-evaluate those selectors once, after all actions have been dispatched, rather than once for each dispatched action.
To demonstrate this, let’s add an expensive selector to our app. (To simulate an expensive selector I’m just blocking the main thread for 3 seconds.)
When we profile this we can see that React Redux is re-evaluating our
expensiveSelector twice (once for each dispatched action):
Why does this happen? Redux notifies subscribers after each successfully dispatched action, and amongst those subscribers will be every
connect ed component in the app.
To optimise this, we can utilise
redux-batched-subscribe, which is a store enhancer that lets you debounce subscriber calls for multiple dispatches.
When we profile this again, we can see that React Redux is now re-evaluating our
expensiveSelector only once:
Note that I used a single expensive selector here for demonstration purposes, but this optimisation may still be worthwhile even if your selectors are not that expensive—React Redux apps tend to use a lot of selectors, so it’s worth considering the aggregate cost of all your selectors when they are all ran at the same time. Furthermore, even if all selectors are correctly memoized (e.g. using reselect), the cost may still be high due to the equality checks React Redux must run on all selector outputs.
All of the code for this article can be found on GitHub and StackBlitz.