Memoization is used to speed up applications by remembering the results of "expensive" function calls and calculations, so that they don't need to be recalculated again when the inputs have not changed.
For example, if I ask you to calculate the results of a multiplication on paper or in your head, say 23 x 27
, then I would expect that it would take you a little time to work out. Then, if I ask you to calculate it again with the same values (23 x 27
) would you calculate it again, or would you remember the previous answer and skip the calculation? You would re-use the previous result to save time. This is what memoization does.
Of course in my example the maths is quick and easy for a computer and it's just meant to illustrate the concept. "Expensive" is just a way to describe things that take a longer time to be completed. Expensive for computers could be anything from complex maths, to iterating through large arrays of data to derive an answer.
An example memoization function
If we take a regular function that does a slow calculation, then calling it multiple times will equal multiple long calculations.
1function myFunction(input) { // Do things that take 5 seconds // return the calculated value return output; } myFunction(x); // 5 seconds myFunction(x); // 5 seconds myFunction(x); // 5 seconds
If we memoize this function we can save time on later calculations. The first time we call the function it completes the long calculation and caches the result. Every time we call the function after the first result it accesses the cache and can skip the long function resulting in a quicker return value.
1function myMemoisedFunction(input) { // create a cache property on the function and initialise it if it's not already myMemoisedFunction.memoCache = myMemoisedFunction.memoCache || {}; // Check the cache for our input values // If it has them, then we have doen this before if (myMemoisedFunction.memoCache[input]) { // return the already calculated value return myMemoisedFunction.memoCache[input] } // Otherwize caluclate the slow things // Do things that take 5 seconds // set the cache values for next time myMemoisedFunction.memoCache[input] = output; // return the calculated value return output; } myMemoisedFunction(x); // 5 seconds myMemoisedFunction(x); // 0.01 seconds myMemoisedFunction(x); // 0.01 seconds
This is what the higher order functions of memo
and useMemo
are used for in React.
React
To understand why it can improve the performance you need to understand a few things about React.
- Components re-render during their lifecycle.
- When a parent re-renders, all it's children and descendants also re-render.
- When a component re-renders, it will recalculate variables.
- Recalculating has an overhead.
- If the calculation is expensive it will take time, and the application will be less performant.
To counteract this issue react has 2 functions that can be used to memoize variables and components. These are the useMemo
hook and the memo
function. Both are higher-order functions - namely are used to wrap other functions (and functional components) to cache the results of expensive calculations and renders.
The useMemo
hook
The purpose of useMemo
is to optimize variable recalculation so that expensively generated variables only need to be calculated once, or if an argument changes.
Note: Even thoughmemo
can speed up an application, it does have an overhead, so using it on everything could be detrimental. The use case foruseMemo
is to cache the results of expensive functions and shouldn't be used on all calculations.
useMemo
consists of 2 parts:
- An anonymous function that takes our expensive calculation which want to memoize.
- A dependency array that lists the variables that would cause the result to change - so it will only recalculate if one of these changes.
1useMemo( ()=> { /* expensive function */ }, [/* Array of dependencies */])
E.g:
1const memoizedResults = useMemo( ()=> reallyReallyComplexAndSlowCalculation(x, y), [x, y])
The dependency array is used to determine if and when to re run the function.
In the following contrived example the a + b
will only be run if a
or b
change.
1const memoizedResults = useMemo(()=> { return a + b }, [a, b]);
The memo
function.
Memoization can also be used for components, however you need to use the memo
function to wrap your component. This can either be done when you declare your component, or when you export it.
1const MyComponent = React.memo(({a, b}) => { // ... the component }) // or const MyComponent = ({a, b}) => { // ... the component }; export default React.memo(MyComponent);
Here is an example component that renders the result of a
+ b
as c
. In this example the component will only render the first time, and if either a
or b
change, otherwise it will use the cached rendered version.
1const MyComponent = ({a, b}) => { const c = a + b; return ( <div>{a} + {b} = {c}</div> ) } // Memoize the component export default React.memo(MyComponent);
It is worth noting that if your component usesuseContext
oruseState
hooks as part of its implementation, then any changes to their values will trigger a re-render.
In the following example we have a useState
hook. Only if the setCount
is called by clicking the "More!" button will the count update and trigger a component re-render.
1const MyComponent = ({a, b}) => { const [count, setCount] = useState(0); const c = a + b + count; return ( <div> <button onClick={() => setCount(count++)}>More!</button> <div>{a} + {b} + {count} = {c}</div> </div> ) } // Memoize the component export default React.memo(MyComponent);
Considerations for using memo
It's always worth looking at the profiler to see which components are slow to render and work from there with your optimisation. Even then, there are a few considerations to when you should use memoization on components.
Pure functional components
Because the memo
function compares variables and caches the result, the memo
fucnction should only be used on pure components - ones that will always render the same results given the same props. This means that the memo function will render the component on the first pass and won't render it again unless the props change.
Rendered often
If your component is rendered often then it's worth considering using memo
. If it's not rendered often then the optimisation may be unnecessary.
Uses the same props
If you have a component where the props provided to it are often the same then you can optimise with memo
. If the props change regularly then there is no sense in optimising since the re-render with the same values will never (or rarely) be used and caching the values is an unnecessary overhead.
Final thoughts
useMemo
and memo
should have an integral part in most complex React applications, however their usage should be considered carefully as there is an overhead to using them. Using the profiler to detect and measure your performance gains should be a key part of your development cycle.
If you use both useMemo
and memo
correctly then you will save your app from needless re-renders and you will have a more optimised and performant app.