Profiling React #1: batched updates

Kai Wohlfahrt
Oct 23, 2019 · 3 min read

tl;dr: If you’re building a React app (or a framework) and want to achieve faster re-rendering on state updates, make sure to look at how Redux uses unstable_batchedUpdates from the react-dom package.

Over the last few months we’ve been working on a state management framework for React. One of the things we were hoping to gain by using immer.js internally was improved performance due to more selective re-rendering, especially when updating state that affected a small number of components. Our initial benchmark was a visualization of a prime sieve up to 10,000:

Screenshot of the prime sieve benchmark

Initially, we were sort-of right. The Prodo version slightly edged out Redux, beaten only by MobX. However, it took far longer to render the first few updates, where many cells were changed, than later ones, while Redux went through the whole sequence at a slower but more consistent pace.

To dig into why, we built an even more reduced benchmark, which just changes a random selection of a large grid. Some quick profiling with Firefox revealed we were spending most of our time in the React library, doing some sort of state update. The narrow tower on the left-most side shows the actual state update.

Flame graph of state update showing 300 ms spent in React
Flame graph of naïve state update

Meanwhile, Redux achieves the same thing with React doing much less work.

Flame graph showing 40 ms spent in React
Flame graph of Redux’s state update

Internally, Prodo groups state updates into actions, with each action applied atomically. As actions can be asynchronous, it awaits the completion of each action and then triggers a state update. We found that removing the await from synchronous actions allowed us to match Redux, but this did not allow React to re-render between an action and any child actions dispatched from it. StackOverflow quickly pointed out that within an event handler, state updates are batched into a single update.

However, we were still stuck as to how Redux managed to achieve its performance, and how we could match it without blocking re-renders. Redux had a batch function, but it seemed to be a no-op [1]. After some searching, we found that this secret is cleverly hidden in the Redux docs (of all places!) and wrapped our action commit logic in unstable_batchedUpdates. After the fix, the Prodo version ran in 25s, beating both MobX at 55s and Redux at over 200s.

If you want to take advantage of batching in your own app, remember the following:

  1. Outside of an event handler, use unstable_batchedUpdates
  2. If you call setState from within an event handler, updates are automatically batched
  1. Redux changes the default batch handler on loading
  2. Hat-tip to logrocket for discovering this at the same time and beating me to the post!

We write code for humans.

We write code for humans.

Kai Wohlfahrt

Written by

We write code for humans.