React Hooks — Slower than HOC?
Arnel Enero
1101

This benchmark is indeed flawed.

(But kudos for creating it—that’s a good way to learn and experiment.)

To start off, you’re running React in development mode. The development mode has overhead in order to provide better warnings — but that makes it unsuitable for benchmarks because the results won’t tell you anything about real-world performance. This is why “Use the Production Build” is the first advice on React’s Optimizing Performance documentation page.

Once I run both of them in production mode, I get comparable results:

Hooks     HOCs
(NOTE: these results are still wrong, see below)
175       156
171 164
169 154
181 138
167 159
153 194
151 152
155 152
147 163
160 162

And yet these results are still wrong because these two snippets aren’t running an identical benchmark.

The Hooks version updates the state and measures time in useEffect().

As explained here, useEffect() lets the browser paint the screen before running the effects. From the user’s perspective, useEffect() usually provides a better experience because the initial render doesn’t wait for the effects to run.

Typically in apps, effects don’t immediately change the state (or at least not drastically). It’s usually wasteful to wait for all of them to execute before painting. In fact, that’s a common performance problem in React classes. That’s why useEffect() doesn’t block the browser from painting by default.

As a result of waiting for the first paint, the useEffect() version of this benchmark runs the measurement code later than the class version does.

So it looks “worse”. We’re comparing apples to oranges. Let’s see what the HOC implementation does in this example!

Looking at the source code, it set state and logs time in componentDidMount() lifecycle method which fires synchronously. This means it fires earlier but the user doesn’t actually see anything yet because the frame hasn’t been painted. This looks better on a benchmark but makes the app less responsive.

In order to compare apples to apples, we can try useLayoutEffect() which runs in the layout phase, similar to componentDidMount(). Results on my machine:

Hooks   HOCs
(These rows compare the same thing--although not a very useful one)
121     156
106 170
111 157
141 152
111 156
121 158
105 170
111 162
108 166
108 157

So it seems like the version of this benchmark using Hooks is even a bit faster after we fix them to measure the same thing!

Does it mean that useLayoutEffect() is faster than useEffect()? Absolutely no, that’s a wrong way to think about it. It only means useLayoutEffect() or componentDidMount() runs earlier. But unless your use case is related to layout (e.g. positioning a tooltip), you usually want to let the screen paint first, and later run the effects. This is exactly what useEffect() lets you do.

If we keep state updates are in useEffect() but measure time in useLayoutEffect() (like componentDidMount) we get even better numbers:

Hooks   HOCs
(Don't pass a screenshot like this around--read the text below.
They're not doing equivalent work!)
106     174
104 198
88 149
88 154
88 149
89 163
85 162
90 157
89 164
91 153

There shouldn’t be a big surprise here. The useEffect() version strictly does less work before paint because we first show the initial render result, and then run the effects which change the state. This means there is some flicker on the screen when they get updated. Unfortunately, this benchmark doesn’t represent a realistic UI so I can’t say which solution would be best. Hooks let you choose whether effects should block rendering or not—but it’s up to you to decide what’s a better user experience for your particular use case.

So it seems like Hooks can be fast, and provide more responsible defaults. Are Hooks always faster than HOCs? No! There are cases where one approach wins over the other, and it depends on many things. (Although I’d like to think Hooks were designed for as little overhead as possible.) A benchmark rendering a thousand text nodes and then updating them one time doesn’t really capture the performance challenges happening in real applications. It also doesn’t provide enough context about the use case to consider different tradeoffs.

For me, the conclusion is less about Hooks vs HOCs. It’s more about how important it is to understand what’s happening in the browser when publishing a benchmark. I hope this was useful, and please let me know if there was something unclear in the explanation, or if I missed something!

Benchmarking is hard.