0.5 C
New York
Thursday, February 2, 2023

Heavy Computation Made Lighter: React Memoization


It’s critical for developers to create apps that function well. A one-second delay in load time can result in a 26% drop in conversion rates, research by Akamai has found. React memoization is the key to a faster client experience—at the slight expense of using more memory.

Memoization is a technique in computer programming in which computational results are cached and associated with their functional input. This enables faster result retrieval when the same function is called again—and it’s a foundational plank in React’s architecture.

React developers can apply three types of memoization hooks to their code, depending on which portions of their applications they wish to optimize. Let’s examine memoization, these types of React hooks, and when to use them.

Memoization in React: A Broader Look

Memoization is an age-old optimization technique, often encountered at the function level in software and the instruction level in hardware. While repetitive function calls benefit from memoization, the feature does have its limitations and should not be used in excess because it uses memory to store all of its results. As such, using memoization on a cheap function called many times with different arguments is counterproductive. Memoization is best used on functions with expensive computations. Also, given the nature of memoization, we can only apply it to pure functions. Pure functions are fully deterministic and have no side effects.

A General Algorithm for Memoization

A simple flowchart shows the logic where React checks to see if the computed result was already computed. On the left, the start node flows into a decision node labeled,

Memoization always requires at least one cache. In JavaScript, that cache is usually a JavaScript object. Other languages use similar implementations, with results stored as key-value pairs. So, to memoize a function, we need to create a cache object and then add the different results as key-value pairs to that cache.

Each function’s unique parameter set defines a key in our cache. We calculate the function and store the result (value) with that key. When a function has multiple input parameters, its key is created by concatenating its arguments with a dash in between. This storage method is straightforward and allows quick reference to our cached values.

Let’s demonstrate our general memoization algorithm in JavaScript with a function that memoizes whichever function we pass to it:

// Function memoize takes a single argument, func, a function we need to memoize.
// Our result is a memoized version of the same function.
function memoize(func) {

  // Initialize and empty cache object to hold future values
  const cache = {};

  // Return a function that allows any number of arguments
  return function (...args) {

    // Create a key by joining all the arguments
    const key = args.join(‘-’);

    // Check if cache exists for the key
    if (!cache[key]) {

      // Calculate the value by calling the expensive function if the key didn’t exist
      cache[key] = func.apply(this, args);
    }

    // Return the cached result
    return cache[key];
  };
}

// An example of how to use this memoize function:
const add = (a, b) => a + b;
const power = (a, b) => Math.pow(a, b); 
let memoizedAdd = memoize(add);
let memoizedPower = memoize(power);
memoizedAdd(a,b);
memoizedPower(a,b);

The beauty of this function is how simple it is to leverage as our computations multiply throughout our solution.

Functions for Memoization in React

React applications usually have a highly responsive user interface with quick rendering. However, developers may run into performance concerns as their programs grow. Just as in the case of general function memoization, we may use memoization in React to rerender components quickly. There are three core React memoization functions and hooks: memo, useCallback, and useMemo.

React.memo

When we want to memoize a pure component, we wrap that component with memo. This function memoizes the component based on its props; that is, React will save the wrapped component’s DOM tree to memory. React returns this saved result instead of rerendering the component with the same props.

We need to remember that the comparison between previous and current props is shallow, as evident in Reacts source code. This shallow comparison may not correctly trigger memoized result retrieval if dependencies outside these props must be considered. It is best to use memo in cases where an update in the parent component is causing child components to rerender.

React’s memo is best understood through an example. Let’s say we want to search for users by name and assume we have a users array containing 250 elements. First, we must render each User on our app page and filter them based on their name. Then we create a component with a text input to receive the filter text. One important note: We will not fully implement the name filter feature; we will highlight the memoization benefits instead.

Here’s our interface (note: name and address information used here is not real):

A screenshot of the working user interface. From top to bottom, it shows a

Our implementation contains three main components:

  • NameInput: A function that receives the filter information
  • User: A component that renders user details
  • App: The main component with all of our general logic

NameInput is a functional component that takes an input state, name, and an update function, handleNameChange. Note: We do not directly add memoization to this function because memo works on components; we’ll use a different memoization approach later to apply this method to a function.

function NameInput({ name, handleNameChange }) {
  return (
    <input
      type="text"
      value={name}
      onChange={(e) => handleNameChange(e.target.value)}
    />
  );
}

User is also a functional component. Here, we render the user’s name, address, and image. We also log a string to the console every time React renders the component.

function User({ name, address }) {
  console.log("rendered User component");
  return (
    <div className="user">
      <div className="user-details">
        <h4>{name}</h4>
        <p>{address}</p>
      </div>
      <div>
        <img
          src={`https://via.placeholder.com/3000/000000/FFFFFF?text=${name}`}
          alt="profile"
        />
      </div>
    </div>
  );
}
export default User;

For simplicity, we store our user data in a basic JavaScript file, ./data/users.js:

const data = [ 
  { 
    id: "6266930c559077b3c2c0d038", 
    name: "Angie Beard", 
    address: "255 Bridge Street, Buxton, Maryland, 689" 
  },
  // —-- 249 more entries —--
];
export default data;

Now we set up our states and call these components from App:

import { useState } from "react";
import NameInput from "./components/NameInput";
import User from "./components/User";
import users from "./data/users";
import "./styles.css";

function App() {
  const [name, setName] = useState("");
  const handleNameChange = (name) => setName(name);
  return (
    <div className="App">
      <NameInput name={name} handleNameChange={handleNameChange} />
      {users.map((user) => (
        <User name={user.name} address={user.address} key={user.id} />
      ))}
    </div>
  );
}
export default App;

We have also applied a simple style to our app, defined in styles.css. Our sample application, up to this point, is live and may be viewed in our sandbox.

Our App component initializes a state for our input. When this state is updated, the App component rerenders with its new state value and prompts all child components to rerender. React will rerender the NameInput component and all 250 User components. If we watch the console, we can see 250 outputs displayed for each character added or deleted from our text field. That’s a lot of unnecessary rerenders. The input field and its state are independent of the User child component renders and should not generate this amount of computation.

React’s memo can prevent this excessive rendering. All we need to do is import the memo function and then wrap our User component with it before exporting User:

import { memo } from “react”;
 
function User({ name, address }) {
  // component logic contained here
}

export default memo(User);

Let’s rerun our application and watch the console. The number of rerenders on the User component is now zero. Each component only renders once. If we plot this on a graph, it looks like this:

A line graph with the number of renders on the Y axis and the number of user actions on the X axis. One solid line (without memoization) grows linearly at a 45-degree angle, showing a direct correlation between actions and renders. The other dotted line (with memoization) shows that the number of renders are constant regardless of the number of user actions.
Renders Versus Actions With and Without Memoization

Additionally, we can compare the rendering time in milliseconds for our application both with and without using memo.

Two render timelines for application and child renders are shown: one without memoization and the other with. The timeline without memoization is labeled

These times differ drastically and would only diverge as the number of child components increases.

React.useCallback

As we mentioned, component memoization requires that props remain the same. React development commonly uses JavaScript function references. These references can change between component renders. When a function is included in our child component as a prop, having our function reference change would break our memoization. React’s useCallback hook ensures our function props don’t change.

It is best to use the useCallback hook when we need to pass a callback function to a medium to expensive component where we want to avoid rerenders.

Continuing with our example, we add a function so that when someone clicks a User child component, the filter field displays that component’s name. To achieve this, we send the function handleNameChange to our User component. The child component executes this function in response to a click event.

Let’s update App.js by adding handleNameChange as a prop to the User component:

function App() {
  const [name, setName] = useState("");
  const handleNameChange = (name) => setName(name);

  return (
    <div className="App">
      <NameInput name={name} handleNameChange={handleNameChange} />
      {users.map((user) => (
        <User
          handleNameChange={handleNameChange}
          name={user.name}
          address={user.address}
          key={user.id}
        />
      ))}
    </div>
  );
}

Next, we listen for the click event and update our filter field appropriately:

import React, { memo } from "react";

function Users({ name, address, handleNameChange }) {
  console.log("rendered `User` component");

  return (
    <div
      className="user"
      onClick={() => {
        handleNameChange(name);
      }}
    >
      {/* Rest of the component logic remains the same */}
    </div>
  );
}

export default memo(Users);

When we run this code, we find that our memoization is no longer working. Every time the input changes, all child components are rerendering because the handleNameChange prop reference is changing. Let’s pass the function through a useCallback hook to fix child memoization.

useCallback takes our function as its first argument and a dependency list as its second argument. This hook keeps the handleNameChange instance saved in memory and only creates a new instance when any dependencies change. In our case, we have no dependencies on our function, and thus our function reference will never update:

import { useCallback } from "react";

function App() {
  const handleNameChange = useCallback((name) => setName(name), []);
  // Rest of component logic here
}

Now our memoization is working again.

React.useMemo

In React, we can also use memoization to handle expensive operations and operations within a component using useMemo. When we run these calculations, they are typically performed on a set of variables called dependencies. useMemo takes two arguments:

  1. The function that calculates and returns a value
  2. The dependency array required to calculate that value

The useMemo hook only calls our function to calculate a result when any of the listed dependencies change. React will not recompute the function if these dependency values remain constant and will use its memoized return value instead.

In our example, let’s perform an expensive calculation on our users array. We’ll calculate a hash on each user’s address before displaying each of them:

import { useState, useCallback } from "react";
import NameInput from "./components/NameInput";
import User from "./components/User";
import users from "./data/users";
// We use “crypto-js/sha512” to simulate expensive computation
import sha512 from "crypto-js/sha512";

function App() {
  const [name, setName] = useState("");
  const handleNameChange = useCallback((name) => setName(name), []);

  const newUsers = users.map((user) => ({
    ...user,
    // An expensive computation
    address: sha512(user.address).toString()
  }));

  return (
    <div className="App">
      <NameInput name={name} handleNameChange={handleNameChange} />
      {newUsers.map((user) => (
        <User
          handleNameChange={handleNameChange}
          name={user.name}
          address={user.address}
          key={user.id}
        />
      ))}
    </div>
  );
}

export default App;

Our expensive computation for newUsers now happens on every render. Every character input into our filter field causes React to recalculate this hash value. We add the useMemo hook to achieve memoization around this calculation.

The only dependency we have is on our original users array. In our case, users is a local array, and we don’t need to pass it because React knows it is constant:

import { useMemo } from "react";

function App() {
  const newUsers = useMemo(
    () =>
      users.map((user) => ({
        ...user,
        address: sha512(user.address).toString()
      })),
    []
  );
  
  // Rest of the component logic here
}

Once again, memoization is working in our favor, and we avoid unnecessary hash calculations.


To summarize memoization and when to use it, let’s revisit these three hooks. We use:

  • memo to memoize a component while using a shallow comparison of its properties to know if it requires rendering.
  • useCallback to allow us to pass a callback function to a component where we want to avoid re-renders.
  • useMemo to handle expensive operations within a function and a known set of dependencies.

Should We Memoize Everything in React?

Memoization is not free. We incur three main costs when we add memoization to an app:

  • Memory use increases because React saves all memoized components and values to memory.
    • If we memoize too many things, our app might struggle to manage its memory usage.
    • memo’s memory overhead is minimal because React stores previous renders to compare against subsequent renders. Additionally, those comparisons are shallow and thus cheap. Some companies, like Coinbase, memoize every component because this cost is minimal.
  • Computation overhead increases when React compares previous values to current values.
    • This overhead is usually less than the total cost for additional renders or computations. Still, if there are many comparisons for a small component, memoization might cost more than it saves.
  • Code complexity increases slightly with the additional memoization boilerplate, which reduces code readability.
    • However, many developers consider the user experience to be most important when deciding between performance and readability.

Memoization is a powerful tool, and we should add these hooks only during the optimization phase of our application development. Indiscriminate or excessive memoization may not be worth the cost. A thorough understanding of memoization and React hooks will ensure peak performance for your next web application.


The Toptal Engineering Blog extends its gratitude to Tiberiu Lepadatu for reviewing the code samples presented in this article.

Further Reading on the Toptal Engineering Blog:



Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles