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.
Background
React performance optimization often focuses on reducing component re‑rendering by caching results. React provides
React.memo,
useCallback, and
useMemofor 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.memomemorizes a functional component's rendered output by shallowly comparing current and next props (using
Object.is). It is analogous to
React.PureComponentfor 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.memoprevents 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,
useCallbackis 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
useCallbackas 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
useCallbackimplemented and used?
Why can overusing
useCallbacksometimes 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
useCallbackhook 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
useCallbackcan 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
useMemocaches 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
useMemoshows it is similar to
useCallbackbut 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:
useMemocaches the result of a computation, while
useCallbackcaches the function itself without invoking it.
Summary
This article summarizes how
React.memo,
useCallback, and
useMemocan 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.
QQ Music Frontend Team
QQ Music Web Frontend Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.