Frontend Development 15 min read

Boost React Performance: Master React.memo, useCallback & useMemo

An in‑depth guide explains how React.memo, useCallback, and useMemo work together to prevent unnecessary re‑renders, includes practical code examples, memoization concepts, source‑code analysis, and best‑practice recommendations for optimizing front‑end performance in modern.

QQ Music Frontend Team
QQ Music Frontend Team
QQ Music Frontend Team
Boost React Performance: Master React.memo, useCallback & useMemo

Background

React performance optimization often focuses on reducing component re‑rendering by caching results. React provides

React.memo

,

useCallback

, and

useMemo

for this purpose, but the official docs warn against overusing these hooks.

Memoization caches the result of an intensive operation and reuses it on subsequent calls, effectively storing function outputs keyed by their inputs.
<code>const memo = function(func) {
    let cache = {};
    return function(key) {
        if(!cache(key)) {
            cache[key] = func.apply(this, arguments);
        }
        return cache[key];
    }
}
memo(testFunc)(arg);
</code>

Using closure to store arguments as keys enables caching, e.g., optimizing a recursive Fibonacci calculation.

React.memo()

React.memo

memorizes a functional component's rendered output by shallowly comparing current and next props (using

Object.is

). It is analogous to

React.PureComponent

for class components, but applies to function components.

Example:

<code>import React, {useState} from 'react';
import Child from './components/Child';

const App: React.FC = () => {
  const [count, setCount] = useState(0);
  const [subData, setSubData] = useState('haha');
  return (
    <div>
      <h1>I am Parent: 被点了{count}次</h1>
      <Child title={subData} />
      <button onClick={() => { setCount(count+1) }}>click</button>
    </div>
  )
}
export default App;

// Child
import React from 'react';

const Child: React.FC<{title: string,}> = ({title}) => {
    console.log('Child render....');
    return (
        <div style={{background: 'gray'}}>
            I am child: {title}
        </div>
    )
}
export default Child;
</code>

In this simple parent‑child example, the child re‑renders even though its props haven't changed. When the child component is large, repeated renders hurt performance.

Wrapping the child with

React.memo

prevents unnecessary re‑renders:

<code>const Child: React.FC<{title: string}> = ({title}) => {
  console.log('Child render....');
  return (
    <div style={{background: 'gray'}}>
      I am child: {title}
    </div>
  )
}
export default React.memo(Child);
</code>

Now the child renders only once; subsequent parent state changes no longer trigger child re‑renders.

When the child needs to modify parent state, a callback prop is introduced, causing the child to re‑render because a new function reference is created on each parent render.

<code>import React, {useState} from 'react';
import Child from './components/Child';

const App: React.FC = () => {
  const [count, setCount] = useState(0);
  const [subData, setSubData] = useState('haha');
  return (
    <div>
      <h1>I am Parent: 被点了{count}次</h1>
      <Child title={subData} onChange={newCount => setCount(newCount)} />
      <button onClick={() => { setCount(count+1) }}>click</button>
    </div>
  )
}
export default App;

// Child
import React from 'react';

const Child: React.FC<{title: string, onChange: Function}> = ({ title, onChange }) => {
  console.log('Child render....');
  return (
    <div style={{background: 'gray'}} onClick={() => onChange(100)}>
      I am child: {title}
    </div>
  );
};
export default React.memo(Child);
</code>

Because the callback prop changes on each render, the child re‑renders. To keep the same function reference,

useCallback

is required.

useCallback

Usage

<code>import React, {useState, useCallback} from 'react';
import Child from './components/Child';

const App: React.FC = () => {
  const [count, setCount] = useState(0);
  const [subData, setSubData] = useState('haha');
  return (
    <div>
      <h1>I am Parent: 被点了{count}次</h1>
      <Child title={subData} onChange={useCallback(newCount => setCount(newCount), [])} />
      <button onClick={() => { setCount(count+1) }}>click</button>
    </div>
  )
}
export default App;
</code>

After applying

useCallback

, the child no longer re‑renders because the callback reference remains stable.

Concept

The official docs describe

useCallback

as a hook that takes an inline callback and a dependency array, returning a memoized version of the callback.

It memoizes the callback function based on its dependencies.

The term “memoized” refers to the caching mechanism explained earlier.

Questions

How is

useCallback

implemented and used?

Why can overusing

useCallback

sometimes degrade performance?

Fiber, introduced in React 16, replaces the stack reconciler to avoid UI thread blocking. Fiber splits work into small units, allowing higher‑priority tasks to interrupt rendering.

Below is a source‑code analysis of

useCallback

(React 16.14):

Source Code Analysis

useCallback

hook definition:

<code>HooksDispatcherOnMountInDEV = {
  useCallback: function (callback, deps) {
    currentHookNameInDev = 'useCallback';
    return mountCallback(callback, deps);
  },
  // ...
};
HooksDispatcherOnUpdateInDEV = {
  useCallback: function (callback, deps) {
    currentHookNameInDev = 'useCallback';
    return updateCallback(callback, deps);
  },
  // ...
};
</code>

Mount phase –

mountCallback

:

<code>function mountCallback(callback, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
function mountWorkInProgressHook() {
  var hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
  };
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
</code>

Update phase –

updateCallback

:

<code>function updateCallback(callback, deps) {
  var hook = updateWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      var prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
function areHookInputsEqual(nextDeps, prevDeps) {
  for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (objectIs(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}
</code>

Conclusion

Implementation details answer the first question. Overusing

useCallback

can hurt performance because each inline function creation consumes memory, and the shallow comparison itself adds overhead. If the dependency array changes frequently, the callback cannot be cached, negating benefits.

useMemo

Extending the previous example, we now pass an object prop to the child, which causes re‑renders because the object reference changes on each parent render.

<code>import React, {useState, useCallback, useMemo} from 'react';
import Child from './components/Child';

const App: React.FC = () => {
  const [count, setCount] = useState(0);
  const [subData, setSubData] = useState('haha');
  return (
    <div>
      <h1>I am Parent: 被点了{count}次</h1>
      <Child
        title={useMemo(() => ({name: subData, age: 1}), [subData])}
        onChange={useCallback(newCount => setCount(newCount), [])}
      />
      <button onClick={() => { setCount(count+1) }}>click</button>
    </div>
  )
}
export default App;
</code>

Now the child still re‑renders because the object prop changes. Using

useMemo

caches the object, preventing unnecessary renders.

Usage

<code>import React, {useState, useCallback, useMemo} from 'react';
import Child from './components/Child';

const App: React.FC = () => {
  const [count, setCount] = useState(0);
  const [subData, setSubData] = useState('haha');
  return (
    <div>
      <h1>I am Parent: 被点了{count}次</h1>
      <Child
        title={useMemo(() => ({name: subData, age: 1}), [subData])}
        onChange={useCallback(newCount => setCount(newCount), [])}
      />
      <button onClick={() => { setCount(count+1) }}>click</button>
    </div>
  )
}
export default App;
</code>

Source‑code analysis of

useMemo

shows it is similar to

useCallback

but invokes the creator function and caches its return value.

Source Code Analysis

<code>HooksDispatcherOnMountInDEV = {
  useMemo: function (create, deps) {
    try {
      return mountMemo(create, deps);
    } finally {
      // ...
    }
  },
  // ...
};
function mountMemo(nextCreate, deps) {
  var hook = mountWorkInProgressHook();
  var nextDeps = deps === undefined ? null : deps;
  var nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
</code>

The key difference:

useMemo

caches the result of a computation, while

useCallback

caches the function itself without invoking it.

Summary

This article summarizes how

React.memo

,

useCallback

, and

useMemo

can be combined to reduce unnecessary renders in React applications. While these hooks are powerful performance tools, they should be applied judiciously, as misuse can introduce overhead without benefits.

PerformanceReactuseCallbackuseMemoReact.memo
QQ Music Frontend Team
Written by

QQ Music Frontend Team

QQ Music Web Frontend Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.