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.