How To Boost React Performance with Memoization Techniques

How To Boost React Performance with Memoization Techniques

Memoization, not memorization, is a technique commonly used in functional programming to enhance application performance. In simple terms, memoization involves caching the result of a function. When the function is called again with the same inputs, the cached result is returned instead of re-executing the function. It is important to note that the memoized function would need to be a pure function. meaning:

  1. The returned value is always the same for an identical set of inputs.

  2. The function does note mutate state outside of its scope, i.e., there are no side-effects.

React, which strongly promotes a functional programming style, offers built-in mechanisms for optimizing applications through memoization. Using React.memo(...), useCallback(...), and useMemo(...), developers can better control their applications, avoiding unnecessary component re-renders and heavy data recalculations.

React.memo(...)

The purpose of React.memo(...) is to avoid unnecessary re-rendering of a child component when its props remain unchanged, even if the parent component re-renders.

The Problem

Consider this <Parent/> component. It contains a button that increments a total state variable when clicked and displays the current value of total. Additionally, it contains a <Child/> component that does not receive any props.

import { useState } from "react"
import Child from "./Child"

const Parent = () => {

    const [total, setTotal] = useState(0)

    return (
        <div className="flex flex-col gap-1 w-1/5">
            <h1 className="text-3xl">Parent Component</h1>
            <div>Total: {total}</div>
            <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
            <div className="mt-3" />
            <Child />
        </div>
    )
}

export default Parent

The <Child/> component only displays how many times it has been rendered. There's a useRef(...) paired with a useEffect(...) that keeps track of the number of renders for the component, and it displays that count.

import { useEffect, useRef } from "react"

const Child = () => {
    console.log("Child Rendered")
    const renderCount = useRef(1)

    useEffect(() => {
        renderCount.current = renderCount.current + 1
    })

    return (
        <div>
            <div className="text-3xl">Child Component</div>
            <div>Render Count: {renderCount.current}</div>
        </div>
    )
}
export default Child

All together these two components create this UI

Let's observe what happens when we click the button to increment the total state variable in the parent component.

The child component re-renders along with the parent component. This seems unusual, particularly because the child component's props are not changing—in fact, it doesn't have any props at all.

The Solution

Fortunately, the solution is straightforward: wrap the child component in React.memo(Component, propsComparatorFunction). Now, when the parent component re-renders, the child component does not.

import { useEffect, useRef, memo } from "react" // import

const Child = () => {
    ...
}

export default memo(Child) // wrapping component

The Comparator Function

React.memo(...) has a second argument for a comparator function, which wasn't utilized in the example. By default, if no function is specified, then React will do a shallow comparison of the previous and new prop values. A shallow comparison is fine if using simple data types like comparing numbers or booleans, but if the prop is something more complex like an object, then a custom implementation might be necessary.

useCallback(...)

The purpose of useCallback(...) is to memoize a function that's defined in the component. If a function is defined in a component, then it will be recreated during each re-render. useCallback(...) can prevent his. The benefit to stopping the recreation of the function is exemplified best when functions are being passed down as props to child components.

The Problem

Consider the same <Parent/> and <Child/> components from before with the same React.memo(...) solution used to prevent re-renders. Except this time <Child/> defines a prop for a callback function, and <Parent/> defines a function called myFunction and passes it down to <Child/>. Also, aside from defining the callback prop, <Child/> doesn't even do anything with it.

import { useState } from "react"
import Child from "./Child"

const Parent = () => {

    const [total, setTotal] = useState(0)

    const myFunction = () => {
        console.log(`NON-Callback function called`)
    }

    return (
        <div className="flex flex-col gap-1 w-1/5">
            <h1 className="text-3xl">Parent Component</h1>
            <div>Total: {total}</div>
            <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
            <div className="mt-3" />
            <Child callback={myFunction} />
        </div>
    )
}

export default Parent
import { memo, useEffect, useRef } from "react"

const Child = ({ callback }: { callback: () => void }) => {

    console.log("Child Rendered")
    const renderCount = useRef(1)

    useEffect(() => {
        renderCount.current = renderCount.current + 1
    })

    return (
        <div>
            <div className="text-3xl">Child Component</div>
            <div>Render Count: {renderCount.current}</div>
        </div>
    )
}

export default memo(Child)

Again, let's see what happens when we change state in the parent component.

All of React.memo(...)'s hard work is undone. Although, React.memo(...) is doing exactly what it's supposed to be doing, but there are two problems:

  1. Each re-render of <Parent/>, myFunction(...) gets recreated completely, and occupies a different space in memory, even though nothing has changed about it.

  2. React.memo(...) is still doing a shallow comparison, but since the old function prop and the new function prop are not referencing the same spot in memory, it thinks that the prop is changed, so <Child/> needs to be re-rendered. Remember functions in JavaScript are objects and objects are compared by reference.

The Solution

Once again, the solution is straightforward: avoid recreating the function and instead utilize the existing function with the useCallback(fn, dependencyList) hook. The first argument defines the function for the necessary work, and similar to useEffect(...), the second argument consists of a list of state variables that useCallback will monitor. As long as none of the state dependencies in the list change, the same function will be returned on every render, rather than creating a new one. Because the function remains unchanged, even from a referential perspective, React.memo(...)'s shallow comparison won't trigger a re-render of <Child/>.

import { useCallback, useState } from "react"
import Child from "./Child"

const Parent = () => {

    const [total, setTotal] = useState(0)

    const myFunction = useCallback(() => {
        console.log(`Callback function called`)
    }, [])

    return (
        <div className="flex flex-col gap-1 w-1/5">
            <h1 className="text-3xl">Parent Component</h1>
            <div>Total: {total}</div>
            <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
            <div className="mt-3" />
            <Child callback={myFunction} />
        </div>
    )
}

export default Parent

useMemo(...)

Lastly, useMemo(...) is used to cache the result of a function. When rendering involves calling a function that produces a value through an expensive calculation, and the inputs to the function are not changing, there's no need to recalculate during each re-render.

The Problem

Continuing from the last code snippet, let's add a new state variable called myName, a function called uppercaseName, and then in the UI let's add a text field and a <p> tag to display the uppercase name.

import { useCallback, useState } from "react"
import Child from "./Child"

const Parent = () => {

    const [total, setTotal] = useState(0)
    const [myName, setMyText] = useState("michael scott")

    const myFunction = useCallback(() => {
        console.log(`Callback function called`)
    }, [])

    const uppercaseName = () => {
        console.log("Processing Name...")
        return myName.toUpperCase()
    }

    return (
        <div className="flex flex-col gap-1 w-1/5">
            <h1 className="text-3xl">Parent Component</h1>
            <div>Total: {total}</div>
            <input className="border-2 p-1" type="text" value={myName} onChange={(e) => setMyText(e.target.value)} />
            <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
            <p>{`Hello, this is ${uppercaseName()}`}</p>
            <div className="mt-3" />
            <Child callback={myFunction} />
        </div>
    )
}

export default Parent

Typing something in the text field will display an uppercase version in the paragraph below the button.

However, if we open the console and start using the button to increment the count, we can see the function is being ran each time during the re-render even though the name has not changed.

The Solution

We can use useMemo(...) to wrap the uppercaseName function similarly to how we used useCallback(...). However, this time the result of useMemo(...) is a value, not a function. Like useCallback(...), it takes a dependency list, and as long as the state variables in the dependency list do not change, the result does not need to be recalculated.

import { useCallback, useState } from "react"
import Child from "./Child"

const Parent = () => {

    const [total, setTotal] = useState(0)
    const [myName, setMyText] = useState("michael scott")

    const myFunction = useCallback(() => {
        console.log(`Callback function called`)
    }, [])

    const uppercaseName = useMemo(() => {
        console.log("Processing Name...")
        return myName.toUpperCase()
    }, [myName])

    return (
        <div className="flex flex-col gap-1 w-1/5">
            <h1 className="text-3xl">Parent Component</h1>
            <div>Total: {total}</div>
            <input className="border-2 p-1" type="text" value={myName} onChange={(e) => setMyText(e.target.value)} />
            <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
            <p>{`Hello, this is ${uppercaseName}`}</p>
            <div className="mt-3" />
            <Child callback={myFunction} />
        </div>
    )
}

export default Parent

Over Optimization

After learning about these optimization techniques, it may be tempting to start using them everywhere. However, this might be overkill for most scenarios. In fact, the React documentation advises using these techniques only for specific cases. Ultimately, it's up to you to decide when and where to use them, based on what is best for your application.

Should you add React.memo everywhere?

Should you add useCallback everywhere?

Should you add useMemo everywhere?

Conclusion

React provides developers with mechanisms to optimize their applications through memoization.

By using React.memo(...), we can limit the number of re-renders of a child component when the parent component re-renders. React.memo(...) will memoize the child component and only re-render it if its props have changed.

Utilizing useCallback(...), we can prevent the unnecessary recreation of functions each time a component re-renders. Additionally, this can be used in conjunction with React.memo(...) to limit child component re-renders when passing functions down as props.

Lastly, useMemo(...) can be used to avoid duplicate recalculations and data processing, especially if it is an expensive operation. As long as the dependent state for the calculation does not change, there is no reason to perform the operation again.

Thank you for taking the time to read my article. I hope you found it helpful.

If you notice anything in the article that is incorrect or unclear, please let me know. I always appreciate the feedback.