A faster React.memo()
“Just show me the code”: click here
I recently spent time optimizing React code, and the obvious answer is, as always, React.memo()
. So to speed things up I
added a bunch of them everywhere because you should memo all the things by default
anyway. This lead me to think if there was a way to make React.memo()
faster. Not that it needs it, I just enjoy a bit
too much performance. And the answer is yes.
And with the compiler not being shipped in React 19, it seems like we’re going to have to keep handling our own memoization for a bit longer, so I might as well share this one with you.
The Code
So to make memo faster (whose signature is React.memo(c: Component, f: CompareFunction)
, btw), we need to write a faster
compare function than the one React has. The good news here is that React being a generic framework kinda needs to expect
its users to do all sorts of funky stuff, whereas we, my friend, can tell our users to just not do anything funky because
we won’t support that.
So let’s start with React’s implementation, and let’s see how we can make it faster.
The first thing I don’t like with this approach is the usage of Object.keys()
. If we’re going to be calling this
function very often, allocating 2 new arrays on each call is nuts. It’s the easy way, but it’s also the wrong way if the
goal is performance (not that it should always be, most of the times, it’s readability). Whatever software you’re
writing, unless you’re dealing with network requests, memory IO is always going to be the biggest cost. Allocating arrays
in RAM is just one example of that. So let’s try to get rid of that.
Another section that I’m not a fan of is this prelude.
What I think happened here is they implemented a generic “shallow equals” function, and re-used it where needed, one
place being React.memo()
. One of the advantages that we have here is that we’re optimizing for one case, and we should
therefore be specializing this function for React props. So let us assume these properties about React props:
- They don’t have funky prototype chains.
- They are not the same object: prop objects are always inline objects.
- They are likely to be the same shape (monomorphic).
- They are likely to be equal: most of the time, only a small fraction of the UI changes.
- ⚠️ They are not
null
and always an object:<div />
is transformed to_jsx('div', {})
. - ⚠️ Having
prop={undefined}
or no prop is functionally equivalent.
I have marked with ⚠️ the assumptions that are unsafe and would lead to an incorrect “generic shallow compare” function, and we’ll come back to these later. With that in mind, here is the implementation I landed on:
As we can assume non-nullable objects, we were able to get rid of the prelude. Comparing the key count to check for equality
is nice, but to avoid using Object.keys()
like in the original we had to do it by iterating each key of the object. You
might be tempted to propose that we return early in the last loop instead of just counting the keys:
The problem problem with that is memory IO again: checking a[key]
loads the value from memory, which is expensive. By
avoiding the early return there, we can avoid touching a
at all again. Depending on which JS engine we’re running on,
we might even not be touching b
again even if we’re iterating its keys, because most engines keep separate memory tables
for an object keys and its values.
One other interesting change we were able to do, because we assumed that “they don’t have funky prototype chains”, is we
were able to remove the Object.hasOwnProperty()
call that React has:
I’ll note that V8 and JavaScriptCore are able to optimize away the hasOwnProperty
call, but SpiderMonkey can’t (yet),
so this optimization is Firefox specific.
Safe version
Some of the assumptions we made above are unsafe however, in particular if you use the pattern 'key' in props
in your
codebase. So here is also the function without the unsafe assumptions, where we have added back the prelude and the check
for the same keys including undefined
ones:
How fast are we?
I’ve benchmarked against a bunch of existing modules, it seems like we’re the fastest1,2! There are various numbers in the table below, but the important part is that we’re about 2x faster than before.
I have compared 2 implementations, one where we don’t include the ⚠️ assumptions, and another one where we do.
1 Alright ok but we do need to admit that we’ve optimized for “react props”, not for general correctness. We
return true
for equals({ a: 1, b: undefined }, { a: 1, c: undefined })
, which works for us but not for a generic compare
function.
We do have fastCompare
which is still the fastest and 1.5x faster than react if we only consider candidates that use safe
assumptions. hughsk/shallow-equals
is discarded due to its ===
use.
2 Alright and yes, I have excluded one package that was faster from this list. In my defense, it was shallow-equal-jit, which requires you to pass the object keys beforehand and only works if the keys stay the same, which does not work for react props. We can assume the keys are likely to be the same, but we can’t assume they are going to be the same.
Final notes
So anyway, if you’re not doing anything funky with your React props and are really way too interested in making your app
performant, you can checkout the repo
or pnpm install react-fast-memo
.
The unsafe version might seem unuseful, but it has certain niche use-cases: if you’re comparing objects that you can 100% guarante have the same shape, then that one should be your pick.
One big asterisk though: on JavaScriptCore, the unsafe version is slower! Up-to-date benchmarks here.
Other notes
“It doesn’t matter for 99% of use-cases”: Yes you’re right.