React Hooks — The Most Performant Way to Write React

By Ryan Carniato

Box Plot of `Partial Update` on JS Frameworks Benchmark (lower is better)

I like to think that I have a pretty good grasp of React Hooks at this point. I’ve been through the trials of learning all the hidden gotchas as I’ve made demos and wrote a library on top of them. This time I returned to an area I’m much more comfortable with: Benchmarking.

Stefan Krause’s JS Frameworks Benchmark might be the greatest holistic benchmark tool for JS library writers to find bugs, notice regressions, and tweak the last ounce performance out of their libraries. It uses a semi realistic scenario of managing operations on large lists, tracking startup time, memory profiling, and raw render performance.

So I decided to add a React Hooks implementation and its currently overall the best performing React variant. Not by a particularly large margin but I’m going to dig in further to see if we can learn anymore from the results. I’m going to compare optimized vanilla JavaScript to React, React Hooks, and React Redux. Understand that these implementations are not strictly equivalent but represent the most optimized approach for their given technology.

This is not my first go at writing implementations for this benchmark. I’ve written four previously including the vanilla JavaScript implementation that we will be using as the baseline here. So I had a good idea of how to pull it together. I came up with a sweet implementation using useState that looked like most other implementations I had done before. It was good and fast but I was passing a lot of callbacks around using useCallback. This was fine but useCallback doesn’t really save you from the overhead on each execution since the function is still created even if it is thrown away for the stored version.

My first idea was to pass setState down to the child component, but that felt strange. Even if Hooks provide this portable primitive it feels strange. Like unlearning years of React to give a child component control over a parent component’s state in a naked way. The clear answer was to use useReducer but I immediately worried that this wasn’t idiomatic enough. Fortunately Dan Abramov was around to clarify the issue and I went that way. Ultimately I’m glad I did as it illustrates one of the key changes Hooks asks of us as developers. We must start thinking of data flows independently of the component boundaries of user interface they represent. This isn’t a new idea, but just one I hadn’t hit in React-land.

The final implementation is as follows:

*** Disclaimer *** I acknowledge the reducer isn’t the most optimized but it was copied from the React Redux implementation and is meant to be idiomatic. Trust me when I say that, while the performance overhead of doing spreads is noticeable in a micro-benchmark, it isn’t important enough versus writing clear idiomatic code. Even the most cut-throat competitors at the top of the benchmark are still using these de-optimizations where it makes sense. React’s performance has to come a long way before concerns like that make any sort of difference.

These tests use Google Chromes Lighthouse startup metrics to capture startup time and time to interactive (TTI). These are arguably the least interesting tests here. As expected React class and Hooks implementations are very similar and any variance is not significant. As expected React Redux is at a disadvantage here since it has to load in an additional library which lends to it’s ~17kb more code size. Having to initiate Redux in addition also hurts it’s startup script time and TTI slightly.

I do want to point one thing out about hooks here. The size of the implementation is smaller in kb’s than the class version. This is likely caused simply by less lines of code(LOCs) required. While not exactly a performance metric since it comes down to code style, but the React Hooks implementation is 109 LOCs versus 158 for React and 151 for React Redux. Given React Redux’s notoriety for boilerplate how does the implementation have less lines than React proper? The React Redux implementation uses function components injecting Redux via HOCs so the code length ends up being roughly the same as using Classes.

React proper is the slight winner here. For the first 3 tests React uses less memory and in the latter 2 Hooks produce better numbers. There are a few reasons for that. Right out the gate both the Hooks and Redux methods need to initialize the reducer which is shown in the ‘ready memory’ test. From there 1000 rows are added during the ‘run memory’ test. At this point Hooks has used ~400kb more memory. What you are seeing mostly is the overhead of all function remove/select handler function creation per row. Where in classes you’d have these functions on the prototype, the hooks version will create these functions per row. On ‘update rows’ they scale up evenly meaning hooks is still behind.

The last 2 tests test full replace and create/clear by repeating the operation 5 times in a row and then measuring at the end state. In both, Hooks slightly are ahead, which suggests that more of their memory can be collected when they are no longer in use. If you look at the vanilla JavaScript implementation, even in that case about 600kb can’t be freed up after create/clear in comparison to ready state. Hooks is ~700kb where React is about 1 megabyte.

I’m not sure if there is too much to draw from here. It looks that generally React with Classes uses less memory than Hooks implementation, but as components are released more memory is freed. In general, like with startup time, memory is not likely going to sway you one way or another.

Now we get to the real heart of it. The confidence interval +- is more important when looking at these tests as the ranges vary on the tests a bit more. One of the more recent updates to this benchmark is that for operations that happen too quickly, like partial update and select row, or ones that traditionally were done on 10,000 rows like clear rows, to be done on 1000 rows with a heavy CPU throttle instead. This drastically reduced the variance on shorter tests. Now only the ‘create many rows’ test, which creates 10,000 rows, has generally more than a single digit confidence interval.

React Hooks in general outperforms the other React solutions across the board. But the comparison might be made clearer with the table below:

This table shows the same information but highlights the statistical significance between the different libraries compared to React Hooks. The red or green highlights indicate that the difference is significant enough to generally say A outperforms B. It is clear that Hooks outperform React Redux across the board. But against React proper the results are only statistically significant on ‘replace all rows’, ‘swap rows’, and ‘append rows’. This may simply be a result of using useReducer which allowed better isolation of the top level Component. It could also be that advantage we saw in memory release has an effect here. The results for ‘create rows’ and ‘clear rows’ are near the significant threshold but are too close to tell for certain.

React Hooks are still something to get accustomed to. Even though I thought I was being very careful and through the course of my refactoring, I still ended up making a buggy commit since I forgot the [] brackets on my useCallback calls at one point. It’s incredibly easy to do. But in general I love the code. It is infinitely more readable and clear on intent for me. Since it compresses on to less lines and promotes the locality of related code I feel like I understand what it’s trying to do much quicker. Look at that implementation. Each function with a specific purpose.

In terms of performance, I’ve seen people compare Hooks to HOCs and since I basically lifted the reducer from the React Redux implementation I figured it was important to compare as well. But comparing native Hooks like useReducer to HOCs like Redux is not a fair comparison at all. All the additional mapping that occurs through the Context API etc has to have overhead. It is not particularly impressive that basically taking the Redux implementation and change it to useReducer is significantly more performant. However, that it can be more performant than classic Class-based React. That is something.

Ultimately, I doubt performance is going to be the decision maker here and I’m sure there are scenarios where the performance needle can point either way. Just know that Hooks are innately fast even before you start going to HOCs to add enhanced behavior to your React Components. Hooks out of the box will give your Class Components a run for their money. And more importantly they encourage you to think about the problem differently. A way that I feel leads to incidentally writing more performant code. But I will let you be the judge of that.