Enhance React Performance with Memoization

Mason Hu
Level Up Coding
Published in
11 min readJan 4, 2022

--

Photo by SpaceX on Unsplash

This is the very first article that I am posting on Medium, and for almost two years I have been brooding over what it was going to be. It didn’t turn out to be the cure for covid or some concretely abstract story talking about the meaning of life, but rather I have decided that I want to share my learnings and experiences with you as a software engineer. The journey of a self-taught developer can often be lonely and frustrating, I suppose through the act of sharing this somewhat makes me feel connected. So let’s grow together 😁

Introduction

React is a front-end Javascript library that allows for seamless creation of component-based single page applications (SPAs). As a web developer in 2022, it is likely that we have used React or at least heard of it at some point in our careers. If your experience with React was like mine, it was easy to get up to speed with the basics such as controlling states with useState or managing side-effects with useEffect. However, soon you realised that the world beyond basic React is vast and that if we want to build performant web applications, especially for data heavy use cases, we have to pull our socks up and start thinking like real software engineers. Therefore, learning about the right tools is essential to improving the performance of your React application.

Today I would like to share with you some tricks I’ve picked up along the way, specifically how to enhance a React application using memoization. We will start with a quick refresher on the following concepts:

  • Memoization
  • React component function revaluation

Armed with the above knowledge, we will then look at how it is implemented in React with the following tools:

  • React.memo()
  • useCallback()
  • useMemo()

Memoization

Memoization is just a fancy word for caching. It is a widely used concept in dynamic programming for code optimisation. You can find an official explanation here, however, here is a short definition before we dive into the code:

Memoization is an optimisation technique that employs a cache that stores results directly mapped to inputs for a function that produced those results. When the function is invoked with previously seen inputs, then instead of running computations the cache is used to return results directly and therefore speeding up the function execution.

The code below demonstrates memoization in Javascript with a simple scenario and some benchmark results.

  1. Generate an array of 10 million random numbers between 0–10 for testing purposes. This is defined globally and used as test data in the following functions.
  2. Setting up some testing metrics for benchmarking functions.
  3. Simple function that adds up all numbers in data from index n .
  4. Benchmark sumNumbersFrom with output: Result: 45004814; Duration: 75ms .
  5. A wrapper function memoizeFunc for producing memoized version of its input function originalFunc . The wrapper establishes a cache cache which is retained by the returned memoized function through its closure. The returned function also take an argument input which should be identical to input required by originalFunc . The cache is first checked to see if input was previously stored, if not, then originalFunc is used to compute the result based on input which is then stored in cache . If input was found in cache then its associated result is returned directly without invoking originalFunc resulting in an O(1) operation.
  6. The wrapper function memoizeFunc is applied to sumNumbersFrom to create its memoized counter part memoizedSumNumberFrom .
  7. Benchmark execution of memoizedSumNumberFrom for both scenarios when results was uncached and cached (for input = 100). We see that in the instance when result was uncached, the function took 75ms to run since the original sumNumbersFrom had to be invoked to compute the result. However, the second time when the same input of 100 is passed to memoizedSumNumberFrom , the cached result is returned immediately resulting in 0ms execution time.

From the above example, we can see that memoization is a neat technique for optimising the time complexity of our functions. We should note at this point that components in React are really just functions and that memoization is perfectly suited to improve the rendering speed of our React application. It is also important to note that memoization comes at the cost of memory for storing the cache and computations required to convert function inputs into keys for caching. Therefore, we should consider implementing this optimisation for computationally intensive problems rather than every little task we encounter!

React Component Evaluation

In this section, we will see the reason why optimisation is required especially for how React evaluates component functions as well as their children components in an application.

Fig.1 Example React App Structure

The above image depicts a very simple React app hierarchy, each block is a component and they are setup in a tree-like structure. The critical aspect to realise about the above structure is that:

React will evaluate/re-evaluate a component when either a state, prop or context change is registered in that component.

When React evaluates a component function, all of its children component will be evaluated regardless of any dependencies that exists between the component and its children. To put it simply, React does not care if, for example, Child 1 depends on any state change in Parent and as long as Parent is evaluated then Child 1 will be evaluated. Same goes for Child 2 and Child 3 .

To demonstrate the above concept, I have created a simple app using create-react-app and the code can be accessed here.

Note: I will be going through only the relevant code sections of the app in this article. It is assumed that everyone who is interested in improving React performance is familiar with how to setup a React project. However, if you want a refresher on how to setup a React project, how the main app component works and gets rendered in the DOM, please refer to the code links provided above.

Demonstration of React component evaluation in the browser
Fig.2 Browser console output from Revaluation component

The above React code uses a Revaluation parent component to demonstrate the structure shown in Fig.1. The parent component contains three child components Child1, Child2 and Child3 of which Child2 is nested in Child1 . The Revaluation component contains a single boolean state toggle which can be updated by toggleHandler .

It is important to note here that changing the toggle state will trigger a re-evaluation for the Revaluation component as well as all its children. Furthermore, neither Child1, Child2 or Child3 is dependent on the state change in Revaluation . Take Child1 for example, it takes in a show prop that is hardwired to false but the console output shown in Fig.2 clearly indicates that Child1 is re-evaluated every time the state is changed in Revaluation . The same observation can be said for Child2 and Child3 i.e. the logic in those child components has nothing to do with the logic in their parent component.

I hope at this point you have come to realise that this section’s example demonstrates a potential performance bottleneck in React applications. In the real world, Child1, Child2 or Child3 may contain computationally intensive logic. Re-evaluating these child components every time due to an unrelated parent component state update is wasteful.

This is where Memoization comes to the rescue. As explained in the earlier section, we can use this optimisation technique to cache results returned from a function based on its inputs. React applications can be sped up using Memoization as they are nothing more than just a bunch of functions nested in a tree-like structure!

React.memo

The first tool we will discuss is React.memo . It is a Higher Order Component (HOC) used to wrap any React functional component for memoization. The code below demonstrates an example for its usage.

React.memo example demonstration in the browser
Fig.3 Browser console output from MemoExample component

The example consists of a parent component MemoExample which contains only a single child component Child1 and a state toggle which can be updated by toggleHandler .

Furthermore, we wrapped Child1 inside of a React.memo call to memoize the child component upon its first render in the DOM i.e. the html syntax tree output by React for Child1 is now cached in memory and mapped to its props. For every subsequent state update registered in MemoExample , React checks the Child1 props for any changes, if no changes took place then the cached output for Child1 will be rendered instead of evaluating the component again. Since Child1 only takes in a single prop show and we have hardwired it to false , this means Child1 will never be evaluated again after its first render regardless of any state changes in MemoExample . This behaviour can be seen in the console output demonstrated in Fig.3 where the console.log in Child1 only ran once and never again for every subsequent changes to the toggle state in MemoExample.

At this point I know what you are thinking, why don’t we just apply React.memo to every component that ever existed under the sun? Well, remember in the section earlier it was mentioned that there is a cost to Memoization? Let us quickly examine some of the caveats of React.memo in order to understand why it is actually a bad idea to use it always:

  • It costs memory to memoize a React component since we need to cache the rendered result as well as props for that component.
  • React.memo only checks for props changes, so for internal state changes the wrapped component will still be evaluated.
  • The props check with React.memo is by default a shallow comparison, meaning that only changes in primitive data types (strings, booleans and numbers) will be properly registered as more complex data types such as arrays, objects or even functions are reference types. To overcome shallow comparison default, we can pass a comparison function as the second argument to React.memo to tell React how the props should be compared exactly.
  • If props for a component contain a lot of data, the comparison cost as well as memory cost may become prohibitive to use React.memo . These trade-offs for speeding up your application should be carefully considered.

useCallback

Before we move on to the next memoization tool in React, let’s address a detail mentioned in the previous section. When using React.memo on components with props that are complex data types, we need to work around the shallow comparison default behaviour. For objects and arrays we can use the comparison function second argument input for React.memo but what if a prop is a function? In Javascript, functions are reference data types which are the same as objects and arrays, therefore shallow comparison for functions will always result in false returns. The below example demonstrates behaviour of React.memo when used for a component with function props.

Browser console output for useCallback example
Fig.4 Browser console output for MemoAndCallback component with functional props

The above MemoAndCallback component is a simple extension of the MemoExample component from the previous section. A Child2 component was added which takes in a single function prop func that is invoked whenever Child2 is rendered. Note that Child2 is wrapped in a React.memo call but as we can see from the console output in Fig.4, Child2 is rendered for every state update in MemoAndCallback . This is due to that the function child2Func is declared for every render of MemoAndCallback , causing it to have a different memory location allocated each time. Because functions are reference types and React.memo compares the newly declared child2Func memory address with the cached child2Func memory address, this causes Child2 being rendered even when its props effectively remains the same for every parent state update.

This is a common problem encountered when using React.memo or any other techniques that require checking data dependencies across render cycles. Due to this, React actually has a hook that addresses this issue and that is the useCallback hook. Below is a modified version of MemoAndCallback component demonstrating its usage.

Fig.5 Browser console output for MemoAndCallback component with useCallback

Here we added yet another component Child3 inside MemoAndCallback also with a functional prop func. The function child3Func is passed to Child3 , however, its declaration is wrapped inside an useCallback hook. The concept of useCallback is exactly the same as React.memo, the actual function being wrapped is cached and if none of the dependencies specified in the second array argument of useCallback changed then the exact same function from the previous render cycle is returned. Using this technique we can ensure that the exact same function, stored in the same memory address, will be passed to a component as a prop. Therefore, React.memo will be able to correctly check props of a component even if they are functions. From the console output in Fig.5, we can see exactly as described above that Child3 only renders once and never again since child3Func remains the same for every state update in MemoAndCallback component.

useMemo

Aside from optimising component functions, React also provides a tool that allows us to memoize any complex expressions that produce some result. The useMemo React hook can be used to wrap code we call inside a component for optimisation, code below demonstrates its usage.

Fig.6 Browser console output for UseMemoExample component

The UseMemoExample component has a single state revaluate which can be updated by revaluateHandler . A global data object consisting of 1 million random numbers is defined outside of the component to mimic persistent data passed to the component. The global data is sorted for every render of UseMemoExample . This sorting operation is further benchmarked and the result displayed in ms within the component. From the output shown in Fig.6 we can see that data is sorted in approximately 200ms every time UseMemoExample is updated. In reality, this sorting operation can be replaced by even more complex logic and could cause the component to render with sub-optimal performance. This is where we can use useMemo to optimise our code, below is a demonstration.

Fig.7 Browser console output for UseMemoExample component with useMemo

The only change we introduced in UseMemoExample component is moving the sorting logic for data into a useMemo hook as a callback where the second argument for the hook is a list of dependencies to be checked for memoization. In this example data is a global constant and therefore should not be considered a dependency. We can see from Fig.7 that when UseMemoExample is first rendered, data is sorted in 205ms and cached by useMemo. For every subsequent state update since no dependencies have changed, the cached and sorted data object will be used and therefore resulting in a huge speed up in rendering speed for the UseMemoExample component.

It should be noted that when implementing useMemo for improving your React components, we should consider the exact same pros/cons mentioned previously for React.memo . If implemented for poor use cases, memoization may result in worse performance due to additional space complexity as well as time required for checking function inputs!

Summary

In this article we discussed what is memoization and how we can use relevant tools provided by React to improve performance for our applications. Specifically React.memo and useMemo were covered and explained in detail. The need for useCallback was also discussed for implementing memoization where functions are included in the memoized function inputs. Each concept was explained and demonstrated using code examples so that we can see exactly how these tools operate in the browser.

Phew!! That’s a lot of information 😅 but hopefully all concepts were explained with enough detail and examples to assist you on your React journey. This being my very first attempt at blogging my development journey and I am almost certain that there may be insufficient areas in my writing, therefore I welcome all the criticism and advice for improvements! Thank you.

--

--

Electrical engineer turned to self-taught software engineer / web developer. Passionate to share my journey of learning and development with the world 🤓