React - Memo

Friday, July 24, 2020

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 though memo can speed up an application, it does have an overhead, so using it on everything could be detrimental. The use case for useMemo 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 uses useContext or useState 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.


Other posts


Tagged with: