
一句话:这两个 Hook 是「引用稳定工具」,它们的成本(比较依赖 + 保存缓存 + 维护依赖数组)在「轻量计算」上会超过收益——只在「配合
memo」或「计算确实昂贵」时才值得用。
一、成本模型
useMemo 的收益:避免重复执行「昂贵计算」
useMemo 的成本:
① 每次渲染都要比较依赖数组(逐个 Object.is)
② React 要保存「上一次的值 + 上一次的依赖数组」(内存)
③ 代码复杂度(依赖数组要维护正确)
④ 失效风险(依赖写错 → 完全没用,还给人「已优化」的错觉)
useCallback 同理(它的成本略低,因为不执行计算,但同样要比较依赖与保存函数)
核心判断:
收益 = 省下的计算/渲染成本 × 发生频率
成本 = 每次渲染的「比较 + 缓存」开销
→ 只有当「收益 > 成本」时才值得。
对于「a + b」这种计算,收益 ≈ 0,成本 > 0 → 不值得。
对于「10 万条数据的排序」,收益很大 → 值得。
二、什么时候该用(三个场景)
1) 计算确实昂贵
// ✅ 值得:10 万条数据的过滤 + 排序
const visible = useMemo(() => {
return hugeList
.filter((it) => it.name.includes(keyword))
.sort((a, b) => b.score - a.score);
}, [hugeList, keyword]);
// ❌ 不值得:简单计算
const fullName = useMemo(() => `${first} ${last}`, [first, last]);
const total = useMemo(() => a + b, [a, b]);
const count = useMemo(() => items.length, [items]);
怎么判断「昂贵」?
① 数据量:是否在「几千」以上?是否有嵌套循环?
② 复杂度:是否是 O(n log n) 或更高?是否包含正则/日期/格式化?
③ Profiler 证据:这个计算在整体渲染耗时中是否占显著比例?
经验阈值:**超过 1ms 的计算值得考虑缓存**(但要用 Profiler 验证)。
2) 作为其它 Hook 的依赖
// ✅ 值得:保持引用稳定,避免 Effect 反复执行
const config = useMemo(() => ({ url, method, headers }), [url, method, headers]);
useEffect(() => {
fetchData(config);
}, [config]); // ⚠️ 如果不 memo,config 每次渲染都是新对象 → Effect 每次都跑
3) 配合 React.memo 稳定 props
// ✅ 值得:子组件是 memo 的,需要稳定 props
const Row = React.memo(RowComponent);
function List({ items, onDelete }) {
const handleDelete = useCallback((id: string) => onDelete(id), [onDelete]);
const options = useMemo(() => ({ showIcon: true }), []);
return (
<ul>
{items.map((it) => (
<Row key={it.id} item={it} onDelete={handleDelete} options={options} />
))}
</ul>
);
}
三、什么时候是负担(四种场景)
// ① 廉价计算
const doubled = useMemo(() => count * 2, [count]); // ❌
const text = useMemo(() => String(value), [value]); // ❌
// ② 常量(应该提到组件外)
const OPTIONS = useMemo(() => ({ a: 1, b: 2 }), []); // ⚠️
const OPTIONS2 = { a: 1, b: 2 }; // ✅ 模块级常量
// ③ 子组件不是 memo
function Parent() {
const handleClick = useCallback(() => {}, []); // ⚠️ 无意义
return <Child onClick={handleClick} />; // Child 没 memo → 照样渲染
}
// ④ 传给原生 DOM 元素
const onClick = useCallback(() => {}, []); // ⚠️ 无意义
return <button onClick={onClick} />; // 原生元素不是组件
| 场景 | 该用吗 | 理由 |
|---|---|---|
| 大数组排序/过滤/聚合 | ✅ | 计算昂贵 |
作为 useEffect/useMemo 的依赖 |
✅ | 引用稳定(语义需要) |
传给 React.memo 子组件 |
✅ | 让 memo 生效 |
a + b、items.length |
❌ | 计算几乎免费 |
| 常量字面量 | ❌ | 应该提到模块作用域 |
| 子组件没 memo | ❌ | 稳定了也没用 |
| 传给原生 DOM 元素的回调 | ❌ | 原生元素不参与 memo |
| 「为了消除 lint 警告」而包一层 | ⚠️ | 要看清提示的内容 |
// ③ 的一个「看起来对但实际无效」的例子
function Parent({ items }) {
const handleClick = useCallback((id: string) => doSomething(id), []); // ✅ 稳定
return (
<div>
{items.map((it) => (
<Row key={it.id} item={it} onClick={() => handleClick(it.id)} />
// ↑ ⚠️ 这里又创建了新函数!
))}
</div>
);
}
// → useCallback 白做了:真正的 props(onClick)依然是内联箭头函数
// ✅ 修复:传 handleClick 本身 + 用 data-* 取 id,或把 Row 的 onClick 也稳定
<Row key={it.id} item={it} onClick={handleClick} />
四、依赖数组的隐性成本
// ⚠️ 依赖「不稳定」时,useMemo 完全失效(还多了一份比较开销)
const value = useMemo(() => compute(a), [a, { extra: 1 }]); // ❌ 每次都重算
const value2 = useMemo(() => compute(a), [a, items.filter((x) => x.on)]); // ❌
// ⚠️ 依赖链越长,维护成本越高
const a = useMemo(() => f(x), [x]);
const b = useMemo(() => g(a), [a]); // a 变了才重算
const c = useMemo(() => h(b, y), [b, y]);
// → 好处是「只有相关依赖变化才重算」,代价是「链条越长越难追踪」
「层层 useMemo」的两个问题:
① 可读性下降(代码变长、逻辑被包裹)
② 一旦某个依赖不稳定,链条上的所有 memo 都失效
替代方案(现代 React 的思路):
· 把「派生计算」提取到「组件外」的纯函数(更易测试、也更容易被 Compiler 优化)
· 把「昂贵计算」放到 Web Worker(真正不阻塞主线程)
· 用 React Compiler 自动记忆化(不再手写)
五、实践决策树
我要不要给这段代码加 useMemo/useCallback?
① 这段逻辑是「引用稳定」需要的吗?
(作为 Effect 依赖 / 传给 memo 子组件 / 作为其它 Hook 的依赖)
├─ 是 → 加(这是「正确性」需要,不只是性能)
└─ 否 → 继续
② 计算是否「确实昂贵」?(> 1ms,或 Profiler 显示它占比显著)
├─ 是 → 加,并用 Profiler 验证收益
└─ 否 → 不加
③ 它是不是「常量」?
├─ 是 → 提到模块作用域(比 useMemo 更好)
└─ 否 → 不加,直接写
// 决策树的应用示例
function Component({ items, keyword, onSelect }) {
// ① 作为依赖 → 需要稳定
const query = useMemo(() => ({ keyword, limit: 20 }), [keyword]);
useEffect(() => {
search(query);
}, [query]);
// ② 昂贵计算 → 值得
const visible = useMemo(
() => items.filter((it) => it.name.includes(keyword)).sort(byScore),
[items, keyword]
);
// ③ 常量 → 提到外面
// const OPTIONS = { showIcon: true }; ← 写模块级
// ④ 廉价计算 → 直接算
const count = visible.length;
const isEmpty = count === 0;
// ⑤ 传给原生元素 → 不需要 useCallback
return (
<ul>
{visible.map((it) => (
<li key={it.id} onClick={() => onSelect(it.id)}>{it.name}</li>
))}
</ul>
);
}
六、React Compiler 的影响
// 有 React Compiler 后:写「朴素代码」即可,编译器自动插入记忆化
function Component({ items, keyword }) {
const visible = items.filter((it) => it.name.includes(keyword)).sort(byScore);
// ↑ 编译器会自动判断是否需要缓存(依赖不变则复用)
return <List items={visible} />;
}
但要注意:
① Compiler 目前是可选特性(需要配置构建工具)
② 它依赖「组件是纯函数」的假设——有副作用的组件会被跳过
③ 它【不能替代】理解:跨组件的引用传递、外部 store 订阅、`key` 等问题它管不了
④ 所以「知道什么时候该优化」依然重要——只是「手写 useMemo」的频率会下降
一句话:Compiler 让「默认性能」变好,但不改变「性能优化需要测量与理解」这件事。
七、常见坑
// ① 无脑给所有回调加 useCallback
const onClick = useCallback(() => {}, []); // ⚠️ 传给原生 div 时无意义
const onChange = useCallback((e) => setV(e.target.value), []);
// → 代码噪音大、收益为零
// ② 依赖数组写错(漏了 → 结果过期;多写了 → 完全失效)
const v = useMemo(() => f(a), []); // ❌ 漏了 a
const v2 = useMemo(() => f(a), [a, {}]); // ❌ 依赖不稳定
// ③ 在 useMemo 里做副作用
const v3 = useMemo(() => {
localStorage.setItem('x', 'y'); // ❌
return compute();
}, []);
// ④ 认为「useMemo 保证只算一次」
// → React 保留丢弃缓存的权利(它是优化提示,不是语义保证)
// ⑤ 把常量用 useMemo 包起来(应该提到组件外)
const OPTIONS = useMemo(() => ({ a: 1 }), []); // ⚠️
// ⑥ 子组件没 memo,父组件却写了一堆 useCallback
// ⑦ 「为了消除 eslint 警告」把函数包进 useCallback 但依赖写成 []
const handle = useCallback(() => { console.log(user); }, []); // ❌ 闭包陷阱
// ⑧ 层层 useMemo 导致可读性崩坏
// ⑨ 用 useMemo 缓存「渲染成本低的对象」,但下游组件也没 memo
// ⑩ 在 memo 子组件里传「每次 new 的对象」作为 props(抵消了 memo 的作用)
// ⑪ 认为「加了 useMemo 就一定能提升性能」(没有测量)
// ⑫ 忘了「函数组件每次渲染都会创建新函数」这个事实,误以为「直接写就行」
// ✅ 对:不传给 memo 子组件时,直接写更清晰
八、面试延伸
- 「
useMemo/useCallback什么时候该用?」
三种场景:① 计算确实昂贵(大数组排序/过滤/聚合,或 Profiler 显示它占显著耗时);② 作为其它 Hook 的依赖(保持引用稳定,避免useEffect反复执行)——这是「正确性需要」而非性能优化;③ 配合React.memo(稳定传给 memo 子组件的 props,让 memo 生效)。除此之外通常不该用。 -
「什么情况下
useMemo/useCallback是负担?」
四种:① 廉价计算(a + b、items.length)——比较成本可能超过计算成本;② 常量——应该提到模块作用域(那里的引用天然稳定);③ 子组件不是memo——稳定了 props 也没用(子组件照样渲染);④ 传给原生 DOM 元素的回调——原生元素不参与 memo。此外还有「为了消除 lint 警告而机械包裹」与「层层 useMemo 导致可读性下降」。 -
「怎么判断一个计算是否『昂贵』?」
三个角度:① 数据量与复杂度——是否处理「几千条以上」的数据?是否 O(n log n) 或更高?是否包含排序、正则、日期格式化、深拷贝?② 调用频率——是否在「每次输入/滚动」时都会执行?③ Profiler 证据——在火焰图里这个计算是否占了渲染耗时的显著比例。经验阈值是「超过约 1ms 的计算值得考虑缓存」,但必须以测量为准,不能凭感觉。 -
「为什么说『作为依赖』是使用它的正当理由?」
因为useEffect(fn, [dep])里的dep若是一个「每次渲染都新建的对象/数组/函数」,就会导致 Effect 每次渲染都执行(甚至无限循环)。这不是「性能问题」而是「语义问题」——Effect 的依赖数组表达的是「什么时候需要重新同步」,而引用不稳定会让这个语义失效。所以「为了稳定依赖而useMemo/useCallback」是在修正语义,属于必须做的事。 -
「
useMemo能保证计算只执行一次吗?」
不能。React 官方明确说明「useMemo是性能优化手段,不是语义保证」——React 保留在必要时丢弃缓存的权利(比如为了释放内存)。所以不要把「只该执行一次」的逻辑放在useMemo里(如初始化第三方库、发请求、注册监听)。需要「只执行一次」应该用useState的惰性初始化、useRef的延迟初始化,或useEffect(..., [])。 -
「为什么给原生元素传
useCallback没有意义?」
因为React.memo只对「组件」生效。原生 DOM 元素(<div>、<button>)不是组件,不会被 memo 包裹,也不存在「props 浅比较跳过渲染」的机制——所以无论onClick的引用是否稳定,父组件渲染时这个元素都会被重新创建并应用新属性。所以这类回调直接写成内联函数更清晰(除非它本身要作为别的 Hook 的依赖)。 -
「怎么看待『层层 useMemo』?」
它是「A → B → C 的派生链」的常见写法:a = useMemo(f, [x])、b = useMemo(g, [a])、c = useMemo(h, [b, y])。好处是「只有相关依赖变化才重算」;代价是「可读性下降」与「一旦某个依赖不稳定,整条链都失效」。现代做法倾向于:① 把派生计算做成「组件外的纯函数」(易测试、易被 Compiler 优化);② 只在「确实昂贵」的那一步加 memo;③ 依赖 React Compiler 自动处理记忆化。 -
「
useMemo/useCallback与性能优化的关系是什么?」
它们是「工具」而不是「优化本身」。性能优化的顺序应该是:① 先测量(Profiler 找热点);② 优先用结构性手段(状态下移、组件拆分、children透传、虚拟滚动、代码分割);③ 最后才用memo+useMemo/useCallback做精细化控制。很多项目的问题不是「缺 memo」,而是「渲染范围太大」「DOM 太多」「包太大」——这些是memo解决不了的。
一句话速记
useMemo/useCallback是引用稳定工具,只在「配合memo」「计算确实昂贵(> ~1ms,Profiler 验证)」「作为其它 Hook 的依赖(语义需要)」时值得用;负担场景是「廉价计算、常量(应提到模块外)、子组件没 memo、传给原生元素」;依赖不稳定会让它完全失效(先修依赖);useMemo不是语义保证(React 可丢弃缓存,别放「只执行一次」的逻辑);「层层 memo」伤可读性;优化的正确顺序是「先测量 → 结构性手段(下移/拆分/children/虚拟滚动/分包)→ 再 memo 微调」,React Compiler 会逐步接管手写记忆化。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!