23 May 2019
When is React's useMemo hook actually worth it?
Hooks were introduced into the stable release in the 16.8 React update. One of them is useMemo, designed to help with performance and unnecessary calculations. When is it actually worth using? The docs say we should use it whenever we make “expensive” calculations. But what is expensive? Will it impact performance when we do it unnecessarily. And by how much? These are the questions I’m going to attempt to answer today.
Since it’s all about performance, if you are not interested in a lengthy explanation, I’ve made a too-long-didn’t-read version just for you. However, be aware that it doesn’t contain a lot of interesting insights you can find in the rest of the article. After all, there is only so much time you can save without compromising quality. Let’s cut to the chase:
- If you useMemo excessively, performance may suffer by up to 11% (longer mounting and updating).
- Most of the time it probably won’t matter.
- Use it whenever you feel like and when doing some seriously heavy stuff, such as hundreds of iterations, parsing JSONs or complicated strings operations.
- When dealing with referencables and passing them to components optimised with React.memo or PureComponents, it’s worth it as well (useCallback too!).
- And, of course, always useMemo your context value.
What is useMemo?
You probably know the basic definition of useMemo so I’m not going to bore you with it. What’s important is that useMemo allows you to use memoization to speed up your app. useCallback does the same, but returns a function – basically a special useMemo case, with cleaner syntax. If you want to know more, hop in for a quick one into the React docs.
The question is: when should we useMemo? There are many opinions on this, ranging from never using it, due to possible negative impact on performance, to using it literally everywhere. Others, like me, wonder: where is the golden mean? Let’s find it!
useMemo testing – methodology
I’ve prepared a test project available on my Github. Main points:
- In every scenario, the web page is reloaded multiple times using window.location.href to simulate a page opening and measure mounting performance as well.
- Performance data between scenarios is stored in sessionStorage (why not localStorage? It persist way too long for this use case). Saving and loading should not affect the measurements as it happens outside of the profiler.
- To measure performance, I use an experimental React profiler component that supposedly is ready to be merged and made stable.
What I am testing here is just the hook performance. I am ignoring benefits that come from using referenceable objects when passing them to other components that are optimised in regards to rendering (especially with PureComponent or React.memo).
The Profiler is a new React component that is currently under review and waiting to be merged and released. It allows for simple and effective performance measurements with a non-complicated API. After rendering its children, it executes a callback with performance data. Here, I will make use of:
- actualDuration – “time spent rendering the Profiler and its descendants for the current (most recent) update”. It will tell me the actual performance during the render. In most applications, it should be quite spikey depending on the actual DOM changes.
- phase – “mount” or “render” – pretty much self-explanatory. Whether the component mounts or only rerenders.
Why not devtools? Using devtools to measure performance is time consuming when testing multiple scenarios, as it relies on manual review. Also, as a Frontend Developer I need to stay on the top of the bleeding edge, and this research provided an opportunity to do so. With the Profiler component, it is possible to conduct A/B testing, measure performance in real-world conditions, on actual clients, and so much more! For now though, as it is not yet fully released, it’s not available in the production build.
Is this the “proper” way? I don’t know for sure. I am open to all criticism, even of the non-constructive kind 🙂
useMemo testing – scenarios
First, let’s discuss the terminology.
- Mounting – occurs when a component is rendered for the first time.
- Render – every render () call or a functional component call.
- Update – when the render result actually changes and the changes are reflected in the DOM. I call it like this because of shouldComponentUpdate
It’s easy to succumb to the temptation to add more and more cases, bloating the data. That’s why I decided to limit myself to:
- Low, medium and high workloads
- Low – multiply and divide by 2 twice,
- Medium – stringify and parse a one-parameter object,
- High – stringify and parse a one-parameter object 10 times.
- Low – multiply and divide by 2 twice,
- With useMemo or without.
- A lot of rendering with sparse updates or a lot of rendering with common updates
- Update every sixth render,
- Update every second render.
This results in 12 scenarios. Aside from them, there were 2 mounting-only scenarios, with and without the useMemo hook. Everything was run on a MacBook Pro 13″ 2.3GHz dual-core i5, with fans cracked to the max to limit thermal throttling, the ever-present performance bottleneck in Apple products.
See also: Code splitting with React & Webpack
useMemo testing – results
If you like raw data, this section is for you.
Keep in mind that any differences should come only from the useMemo hook, as everything else should be the same for scenarios with/without useMemo. What can we learn from the data?
- Mounting and updating performance was 5-11% lower with the useMemo hook in comparison to no hook.
- Difference between average render times in scenarios with the same workload and useMemo status are very small, which suggests that the Profiler component presents repeatable and trustworthy results.
- On average, when mounting and updating are included, useMemo is slower only with very trivial loads.
- Only with heavy load is the render (no update, no mount) performance significantly affected – left side of the chart is significantly lower, while the right side is slightly higher with useMemo. When the value changes more often, the average time goes up as well.
useMemo testing – conclusions
The performance cost that comes with using useMemo in React isn’t big, and the potential benefits seem to be quite large. While testing, it was 10,000 useMemos vs 0 useMemos – a situation that’s unlikely to occur in the real world. Initial mount performance suffers a little, but subsequents renders can be sped up by a lot. If your goal is to perform the first render as fast as possible and you are looking to shave even the smallest margins, you should do the rendering on the server side.
Now, that I made this test, I believe that it’s better to useMemo excessively rather than use it sparingly. At the end of the day, it still comes down to two options – use your judgment or measure the impact.