讲解
React 的默认行为是「父组件渲染,子组件全部跟着渲染」。大部分时候这没问题(diff 很快),但当子树很大或渲染昂贵时就要介入。三件套各司其职:
memo 包住组件——props 浅比较没变就跳过重渲染:const ExpensiveList = memo(function ExpensiveList({ items }) {...})。注意它比较的是 props 引用,父组件每次渲染新建的对象/函数 props 会让 memo 失效,于是需要后两件套配合。
useMemo 缓存计算结果:const sorted = useMemo(() => [...items].sort(...), [items]),依赖没变就不重算,返回同一个引用。两个用途:避免昂贵计算每次渲染都跑;给 memo 子组件提供稳定的对象 props。
useCallback 缓存函数:const handle = useCallback((id) => ..., [deps]),等价于 useMemo 返回函数。用途单一:给 memo 子组件传稳定回调,或放进 effect 依赖数组。
最重要的告诫:先测量,后优化,默认不加。memo 三件套本身有成本(比较、缓存、心智负担),乱加会让代码复杂而性能毫无变化。正确的触发点是:Profiler 面板显示某子树重渲染耗时明显,或大数据列表肉眼掉帧。还有一个结构性替代方案常常更好用——「状态下沉 + children 组合」:把变化的状态挪到小组件里,或把不变的部分作为 children 传下来(children 是 props,引用不变就不会重渲染),很多时候不用 memo 就解决了问题。
示例
量化「memo 跳过渲染」的收益,并验证稳定引用的必要性(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 模拟 memo 的浅比较:props 全部引用相同 → 跳过渲染
function shallowEqual(a, b) {
const ka = Object.keys(a);
const kb = Object.keys(b);
return ka.length === kb.length && ka.every((k) => Object.is(a[k], b[k]));
}
let childRenders = 0;
function renderChild(props, prevProps) {
if (prevProps && shallowEqual(props, prevProps)) return '跳过(memo 命中)';
childRenders++;
return '渲染';
}
// 父组件渲染 3 次:内联对象/函数 → memo 失效
let prev = null;
for (let i = 0; i < 3; i++) {
const props = { items: [1, 2, 3], onSelect: () => {} }; // 每次新建引用
renderChild(props, prev);
prev = props;
}
assert.strictEqual(childRenders, 3); // 每次都渲染
// useMemo/useCallback 稳定引用后 → 只渲染一次
childRenders = 0;
const stableItems = [1, 2, 3]; // useMemo 缓存
const stableFn = () => {}; // useCallback 缓存
prev = null;
for (let i = 0; i < 3; i++) {
const props = { items: stableItems, onSelect: stableFn };
renderChild(props, prev);
prev = props;
}
assert.strictEqual(childRenders, 1);
// useMemo 的缓存语义:依赖没变不重算
let computeCount = 0;
function useMemoSim(compute, deps, prevDeps, prevValue) {
const same = prevDeps && deps.every((d, i) => Object.is(d, prevDeps[i]));
if (same) return prevValue;
computeCount++;
return compute();
}
const big = [5, 3, 8, 1];
let sorted = useMemoSim(() => [...big].sort((a, b) => a - b), [big], null, null);
sorted = useMemoSim(() => [...big].sort((a, b) => a - b), [big], [big], sorted);
assert.strictEqual(computeCount, 1); // 第二次直接命中缓存
assert.deepStrictEqual(sorted, [1, 3, 5, 8]);
console.log('内联 props:子组件渲染 3 次;稳定引用:渲染 1 次');
console.log('useMemo 依赖未变,排序只算了一次:', sorted.join(','));
真实代码:
import { memo, useCallback, useMemo, useState } from 'react';
const Row = memo(function Row({ item, onSelect }) {
console.log('渲染行', item.id);
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});
function List({ items }) {
const [selected, setSelected] = useState(null);
// 稳定引用:memo 子组件才能真正跳过渲染
const handleSelect = useCallback((id) => setSelected(id), []);
const sortedItems = useMemo(() => [...items].sort((a, b) => a.name.localeCompare(b.name)), [items]);
return (
<ul>
{sortedItems.map((i) => (
<Row key={i.id} item={i} onSelect={handleSelect} />
))}
</ul>
);
}
常见坑
- memo 配内联 props:
data={{...}}、onClick={() => ...}每次新引用,memo 形同虚设——三件套要配套用。 - 给所有组件包 memo:比较也有成本,浅层小组件 memo 纯属浪费;Profiler 测过再动手。
- useMemo 依赖乱写:依赖漏了拿到旧缓存,多写了缓存失效;听 ESLint。
- 忽略 children 方案:很多「memo 优化」用状态下沉/children 组合就能结构性解决,代码还更简单。
- 在渲染期间做昂贵计算不缓存:大列表排序过滤每次渲染重跑,要么 useMemo 要么源头预处理。
小结
memo 拦子树重渲染,useMemo 缓存值,useCallback 缓存函数;配合稳定引用才生效;先测量再优化,优先考虑 children/状态下沉。下一章把逻辑抽出组件:自定义 Hooks。