Why React Memoisation Usually Doesn't Work
Why React memoisation rarely pays off unless everything is memoised, how referential stability breaks useMemo/useCallback.
Introduction
In React, a component re-renders in response to one of three conditions:
- It’s parent has re-rendered.
- It’s local state has updated.
- It’s context, or the state within the provider, has updated.
Sometimes, the cost to re-render can be expensive and be the cause for a performance bottleneck. This is a problem that memoisation can solve.
The All-Or-Nothing Problem
For memoisation to be effective in a React application, an all-or-nothing approach must be taken. Memoisation only works when every single non-primitive prop and the component itself are memoized. Everything else is just a waste of memory and unnecessarily complicates your code.
This constraint is why memoisation, through use of useMemo and useCallback hooks, is an ineffective and largely unrecommended technique.
Lets have a look at some examples.
Broken Memoisation Example
function Library({ books }: { books: Array<string> }) { const reserveBook = useCallback(() => { // some operation for reserving a book }, [books]);
return <LibraryCheckout reserveBook={reserveBook} />;}Job done, right? Not quite. There are actually two problems here that mean we have not achieved proper, effective memoisation.
- The
LibraryCheckoutcomponent isn’t memoised, so it will re-render every timeLibraryre-renders, regardless of ouruseCallback. - The
booksprop might not be referentially stable. We have no confidence that thebooksprop we have passed as an internal dependency to ouruseCallbackis referentially stable. If it isn’t memoised itself, theuseCallbackhook is contributing nothing because it will be re-run each timebooksupdates. React uses Object.is to compare dependency array props. In order for thereserveBookcallback to remain memoised,booksmust also be memoised because it is not a primitive and therefore redeclaring it can change its reference.
1 is an easy solve. We just wrap the component in .memo. React will skip rendering that component (and its children) if its props are unchanged.
import { memo } from "react";
function Library({ books }: { books: Array<string> }) { const reserveBook = useCallback(() => { // some operation for reserving a book }, [books]);
return <MemoisedLibraryCheckout reserveBook={reserveBook} />;}
function LibraryCheckout({ reserveBook }: { reserveBook: () => void }) {...};
function MemoisedLibraryCheckout = memo(LibraryCheckout);2 is much harder. It requires us to climb the component tree, adventure beyond the scope of the component we are actually concerned with, to see whether or not our books prop is properly memoised. This is a great illustration of why memoisation is a flunky technique.
To foot stamp the ineffectiveness of this type of memoisation, what if books itself was itself the product of another potentially referentially unstable prop? We’d have to walk further up the tree, further from our initial point of inspection and point of memoisation. We are forced to travel an indeterminate memoisation chain.
function City() { // ⚠️ We'd have to memoise books. const books = ["The Great Gatsby", "1984", "To Kill a Mockingbird"];
return ( <Library books={books} /> )}
// ⚠️ We'd have to "memo" the Library component.function Library({ books }: { books: Array<string> }) { const reserveBook = useCallback(() => { // some operation for reserving a book }, [books]);
return <MemoisedLibraryCheckout reserveBook={reserveBook} />;}The job to achieve effective, workable memoisation is completely indeterminate. And if anyone makes a change to the props passed to any other these components without considering memoisation then all this effort is wasted. Memoisation is incredibly fragile and it is easy to break in the future. It requires you to understand context and rules far beyond the component you are actively coding in.
References
Why we memo all the things The all-or-nothing cost of using Reacts idiomatic solution for memoisation.
How to write performant React code Nadia produces some of the best React performance related content on the Interwebs.
The useless useCallback This triggered me to write this. Dominique does a good job of highlighting the fragile and indeterminate memoisation chain.