
一句话:有用,但只在三个场景——「配合
React.memo稳定 props 引用」「计算确实昂贵」「作为其它 Hook 的依赖」;其余情况是负担(比较依赖 + 保存缓存 + 维护成本),所以标准答案是「先测量,再优化」。
一、结论先行
问题:useMemo / useCallback 真有用吗?
一句话回答:
「有用,但要看场景——它们的价值只在『稳定引用』与『避免昂贵计算』,
滥用反而增加成本。所以判断标准是『是否真的减少了工作』,
而不是『看起来更专业』。」
⭐ 三个真正有用的场景:
① 配合 React.memo 稳定 props(⚠️ 前提:子组件是 memo 的)
② 计算确实昂贵(大数组排序/过滤/聚合,> ~1ms)
③ 作为其它 Hook 的依赖(保持引用稳定,避免 Effect 反复执行)
⭐ 四个「是负担」的场景:
① 廉价计算(a + b、items.length)
② 常量(应该提到模块作用域)
③ 子组件不是 memo(稳定了也没用)
④ 传给原生 DOM 元素(原生元素不参与 memo)
二、核心机制:它们解决的是「引用」问题
// 问题根源:每次渲染都会新建「对象/数组/函数」
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<MemoChild
options={{ page: 1 }} // ⚠️ 每次渲染都是新对象
items={[1, 2, 3]} // ⚠️ 新数组
onClick={() => console.log('x')} // ⚠️ 新函数
/>
</>
);
}
// → React.memo 的浅比较失败 → 子组件每次都重渲染
// ✅ 用 useMemo / useCallback 稳定引用
function Parent2() {
const [count, setCount] = useState(0);
const options = useMemo(() => ({ page: 1 }), []);
const items = useMemo(() => [1, 2, 3], []);
const handleClick = useCallback(() => console.log('x'), []);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<MemoChild options={options} items={items} onClick={handleClick} />
</>
);
}
// ✅ props 引用稳定 → React.memo 生效 → 子组件不再无谓重渲染
⭐ 一句话记住:
useMemo 缓存「结果」,useCallback 缓存「函数」——
它们的作用是「让引用稳定」,而不是「让代码变快」。
(useCallback(fn, deps) 完全等价于 useMemo(() => fn, deps))
三、三个真正有用的场景(带证据)
场景一:配合 React.memo 稳定 props
const Row = React.memo(function Row({ item, onSelect }: RowProps) {
console.log('Row 渲染', item.id);
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});
function List({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
// ⭐ 稳定回调引用(否则每一行都会重渲染)
const handleSelect = useCallback((id: string) => onSelect(id), [onSelect]);
return (
<ul>
{items.map((item) => (
<Row key={item.id} item={item} onSelect={handleSelect} />
))}
</ul>
);
}
⭐ 这是最常见的「真正有用」的场景:
长列表 + memo 行组件 + 稳定回调 —— 直接决定滚动/更新的流畅度。
⚠️ 但要注意「两个前提」:
① 子组件确实是 React.memo 的(否则稳定了 props 也没用)
② 「item」本身也要引用稳定(如果每次 filter/map 出新数组,memo 依旧失效)
场景二:计算确实昂贵
// ✅ 值得:10 万条数据的过滤 + 排序
const visible = useMemo(() => {
return hugeList
.filter((it) => it.name.includes(keyword))
.sort((a, b) => b.score - a.score);
}, [hugeList, keyword]);
// ✅ 值得:复杂聚合/统计
const stats = useMemo(() => computeStats(rawData), [rawData]);
// ✅ 值得:正则/日期格式化的批量处理
const formatted = useMemo(() => rows.map(formatRow), [rows]);
⭐ 怎么判断「昂贵」?
① 数据量(几千以上?有嵌套循环?)
② 复杂度(O(n log n) 或更高?)
③ **Profiler 证据**(它在渲染耗时里占显著比例吗?)
经验阈值:超过约 1ms 的计算值得考虑缓存。
场景三:作为其它 Hook 的依赖
// ✅ 值得:保持引用稳定,避免 Effect 反复执行(这是「正确性需要」)
const config = useMemo(() => ({ url, method, headers }), [url, method, headers]);
useEffect(() => {
fetchData(config);
}, [config]);
// ⚠️ 如果不 memo:config 每次渲染都是新对象 → Effect 每次都执行 → 可能无限循环
⭐ 这一条很重要(容易被忽略):
它不只是「性能优化」,而是「**修正 Effect 的语义**」——
依赖数组的含义是「什么时候需要重新同步」,
引用不稳定会让这个语义失效(Effect 每次都执行)。
四、四个「是负担」的场景
// ① 廉价计算(比较依赖的成本 > 计算成本)
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(稳定了 props 也没意义)
function Parent() {
const handleClick = useCallback(() => {}, []); // ⚠️ 白做
return <Child onClick={handleClick} />; // Child 没 memo
}
// ④ 传给原生 DOM 元素(原生元素不是组件,不参与 props 比较)
const onClick = useCallback(() => {}, []); // ⚠️ 白做
return <button onClick={onClick} />;
⭐ 为什么「是负担」?—— 成本清单:
① 每次渲染都要「比较依赖数组」(逐个 Object.is)
② React 要保存「上一次的值 + 上一次的依赖」(内存)
③ 代码复杂度(依赖数组要维护正确)
④ 失效风险(写错 = 完全没用,还给人「已优化」的错觉)
五、最典型的「无效优化」
// ❌ 看起来做了优化,实际完全无效
function List({ items, onDelete }: Props) {
const handleDelete = useCallback((id: string) => onDelete(id), [onDelete]);
// ⭐ 函数引用确实稳定了
return (
<ul>
{items.map((item) => (
<Row
key={item.id}
item={item}
onDelete={() => handleDelete(item.id)} // ⚠️ 这里又创建了新函数!
/>
))}
</ul>
);
}
// → Row 收到的 onDelete 依然是「每次渲染都不同的函数」→ memo 失效
// ✅ 修复一:把「不需要参数的稳定函数」直接传下去
const Row = React.memo(function Row({ item, onDelete }: { item: Item; onDelete: (id: string) => void }) {
return <li onClick={() => onDelete(item.id)}>{item.name}</li>; // ⭐ 箭头函数在 Row 内部(不影响 Row 的 memo)
});
function List({ items, onDelete }: Props) {
const handleDelete = useCallback((id: string) => onDelete(id), [onDelete]);
return (
<ul>
{items.map((item) => (
<Row key={item.id} item={item} onDelete={handleDelete} /> {/* ⭐ 直接传,不在外层包箭头 */}
))}
</ul>
);
}
// ✅ 修复二:data-* + 事件委托 → 处理函数永久稳定
function List2({ items, onDelete }: Props) {
const onDeleteRef = useRef(onDelete);
useEffect(() => { onDeleteRef.current = onDelete; });
const handleClick = useCallback((e: React.MouseEvent<HTMLUListElement>) => {
const id = (e.target as HTMLElement).closest('[data-id]')?.getAttribute('data-id');
if (id) onDeleteRef.current(id);
}, []); // ⭐ 依赖为空 → 引用永久稳定
return (
<ul onClick={handleClick}>
{items.map((item) => <RowWithoutCallback key={item.id} item={item} />)}
{/* ⭐ RowWithoutCallback 的 props 只有 item(稳定)→ memo 永久生效 */}
</ul>
);
}
六、依赖数组的三个常见错误
// ① 漏了依赖 → 结果过期(闭包陷阱)
const sorted = useMemo(() => [...list].sort(cmp), []); // ❌ 应该依赖 [list]
// ② 依赖里有「每次渲染新建的对象/函数」→ 完全失效
const value = useMemo(() => compute(a), [a, { extra: 1 }]); // ❌ 每次都重算
const fn = useCallback(() => f(a), [{ a }]); // ❌ 同上
// ③ 依赖了「整个对象」但只用一个字段
useEffect(() => { doSomething(user); }, [user]); // ⚠️ user 变化频繁
useEffect(() => { doSomething(user.id); }, [user.id]); // ✅ 依赖原始值
⭐ 一个实用的检查清单(写完 useMemo 后自问):
① 依赖数组「是否包含计算里用到的所有响应式值」?(漏了 → 结果过期)
② 依赖里「有没有字面量对象/内联函数」?(有 → 完全失效)
③ 「下游是谁」?—— 是 memo 组件 / Effect?还是「原生元素 / 普通渲染」?
· 前者 → 有价值
· 后者 → 大概率没价值
七、useMemo 的三个「反直觉」事实
① 【不是语义保证】
React 官方明确说:useMemo 是「性能优化手段,不是语义保证」——
React 保留「在必要时丢弃缓存」的权利(如为了释放内存)。
→ 所以「只执行一次」的逻辑不要放在 useMemo 里
(应该用 useState 惰性初始化 / useRef 延迟初始化 / useEffect(..., []))
② 【不保证「不重算」】
因为「React 可能丢弃缓存」,所以「重算」在语义上是允许的。
它保证的是「依赖不变时通常复用」——这是「尽力而为」。
③ 【依赖数组本身有成本】
每次渲染都要「逐项比较」,字段极多时这个成本不可忽略。
→ 所以「依赖一个包含 20 个字段的对象」可能不如「依赖 2 个用到的字段」。
八、常见追问
Q: 那到底该不该「无脑加 useMemo」?
A: 不该。默认不写;只有当「Profiler 显示这里是热点」
或「作为依赖」「配合 memo」时才加。无脑加的结果通常是
「代码更复杂、性能没提升、还引入了隐蔽 bug(依赖写错)」。
Q: useMemo 能提升性能吗?
A: 分场景:
✅ 配合 memo / 昂贵计算 / 作为依赖 → 能
❌ 其它情况 → 通常不能甚至变负(比较成本 + 缓存成本)
Q: 加了 useMemo 却没效果,为什么?
A: 三个常见原因:
① 依赖里有「每次渲染新建的值」→ 每次都重算
② 子组件不是 memo → 稳定了 props 也没用
③ 子组件是 memo 但「还有别的 props 不稳定」→ 一个不稳定就全部失效
Q: useCallback 和 useMemo 的关系?
A: useCallback(fn, deps) === useMemo(() => fn, deps)。
useCallback 只是 useMemo 的「语法糖」。
Q: React Compiler 会取代它们吗?
A: 会「大幅减少手写」,但不会完全取代——
结构性问题(Context、children 透传、虚拟滚动)它管不了,
而且它「跳过不纯的组件」。所以「理解引用稳定」依然必要。
Q: 为什么「传给原生元素」的 useCallback 没意义?
A: 因为 React.memo 只对「组件」生效;
原生元素(<div>/<button>)不是组件,
父组件渲染时它本来就会被重新创建并应用新属性。
Q: 在 useEffect 依赖里放 useCallback 的函数,一定对吗?
A: 只有当「这个函数确实需要稳定」时才对。
如果「函数只在 Effect 内部用」,更好的做法是「把它移进 Effect」
(依赖就变成函数用到的值,更简单也更不容易错)。
九、常见坑
// ① 无脑给所有回调加 useCallback(代码噪音大、收益为零)
// ② 依赖数组写错(漏了 → 过期;多了 → 失效)
// ③ 在 useMemo 里做副作用(应该在 Effect 里)
const v = useMemo(() => { track(); return compute(); }, []); // ❌
// ④ 认为「useMemo 保证只算一次」
// ⑤ 把常量用 useMemo 包起来(应该提到组件外)
// ⑥ 子组件没 memo,父组件却写了一堆 useCallback
// ⑦ 「为了消除 eslint 警告」把函数包进 useCallback 但依赖写成 []
const h = useCallback(() => console.log(user), []); // ❌ 闭包陷阱
// ⑧ 在列表里「外层 useCallback + 内层箭头函数」(外层白做)
{items.map((it) => <Row onDelete={() => handleDelete(it.id)} />)} // ❌
// ⑨ 层层 useMemo 导致可读性崩坏
// ⑩ 用「渲染次数」判断优化效果(应该看耗时)
// ⑪ 认为「加了 useMemo 就一定更快」(可能变慢)
// ⑫ 在 useMemo 的依赖里调用函数(每次渲染都执行)
useMemo(() => f(a), [getKey()]); // ❌
// ⑬ 缓存了「大对象」却忘了「缓存本身占内存」
// ⑭ 把 useMemo 当「计算缓存」用于「跨组件的共享」(那是 Query/store 的活)
十、面试延伸
- 「
useMemo/useCallback真有用吗?」
有用,但只在三个场景:① 配合React.memo稳定 props(让 memo 的浅比较能通过,从而跳过子组件重渲染);② 计算确实昂贵(大数组排序/过滤/聚合);③ 作为其它 Hook 的依赖(保持引用稳定,避免useEffect反复执行——这是「正确性」需要而非纯粹的性能优化)。其它情况通常是负担:廉价计算、常量(应提到模块作用域)、子组件不是 memo、传给原生 DOM 元素。 -
「无脑加
useMemo有什么问题?」
四个成本:① 依赖比较——每次渲染都要逐项Object.is;② 缓存保存——React 要存「上一次的值与依赖」;③ 维护成本——依赖数组容易写错(漏了 → 结果过期;含字面量 → 完全失效);④ 误导性——写错的useMemo看起来「已经优化」,实际毫无作用(还掩盖了真正的瓶颈)。所以规范是「默认不写,需要时再加」。 -
「为什么『传给原生元素的
useCallback』没意义?」
因为React.memo只对「组件」生效。原生元素(<div>、<button>)不是组件,不存在「props 浅比较跳过渲染」的机制——无论onClick的引用是否稳定,父组件渲染时这个元素都会被重新创建并应用新属性。所以这类回调直接写内联函数更清晰(除非它本身要作为别的 Hook 的依赖)。 -
「为什么『子组件不是
memo』时useCallback也白做?」
因为「父组件重渲染时,子组件默认也会重渲染」(见 4.6)——这与「props 引用是否稳定」无关。React.memo是唯一的「用 props 比较来跳过子组件渲染」的机制;没有它,父组件重渲染 → 子组件函数会被重新调用。所以在「没有 memo 的子组件」上稳定 props 是纯粹的浪费(还增加了依赖维护成本)。 -
「为什么说『作为依赖』是用它的正当理由?」
因为useEffect(fn, [dep])里的dep若是「每次渲染都新建的对象/数组/函数」,会导致 Effect 每次都执行(甚至无限循环)。这不是「性能问题」而是「语义问题」——依赖数组的含义是「什么时候需要重新同步」,而引用不稳定会让这个语义失效。所以「为了稳定依赖而 memo」是在修正语义,属于必须做的事(而不是可选的优化)。 -
「
useMemo能保证计算只执行一次吗?」
不能。React 官方明确说明「useMemo是性能优化手段,不是语义保证」——React 保留「在必要时丢弃缓存」的权利(如为了释放内存)。所以不要把「只该执行一次」的逻辑放在useMemo里(如初始化第三方库、发请求、注册监听)。需要「只执行一次」应该用:useState的惰性初始化、useRef的延迟初始化,或useEffect(() => {...}, [])。 -
「加了
useMemo却没效果,怎么排查?」
按顺序查三点:① 依赖数组——里面有没有「每次渲染新建的值」(字面量对象、内联函数、filter出的新数组)?有的话把它稳定掉(提到模块外 /useMemo/ 依赖原始值);② 下游是不是memo组件——如果不是,useMemo白做;③ 下游是 memo 了,但它还有别的 props 不稳定——只要一个 props 引用变了,浅比较就失败,其余 memo 都是白做。排查工具是 Profiler 的「Why did this render?」(它会告诉你哪个 prop 变了)。 -
「React Compiler 之后还需要手写吗?」
手写频率大幅下降,但理解仍然必要。因为:① Compiler 会自动插入记忆化(比手写更精确、更少出错);② 但它只覆盖「局部记忆化」——结构性问题(Context的订阅粒度、children透传、状态下移、虚拟滚动、代码分割)它管不了;③ 它跳过「不纯的组件」(你要能识别哪些代码不纯);④ 跨组件的引用传递仍需理解(否则看不懂「为什么加了 memo 还是重渲染」)。一句话:Compiler 减少机械劳动,不减少理解成本。
一句话速记
useMemo/useCallback只在三个场景有用:「配合React.memo稳定 props(前提:子组件是 memo 的)」「计算确实昂贵(> ~1ms,Profiler 验证)」「作为其它 Hook 的依赖(语义需要)」;其余是负担(依赖比较 + 缓存 + 维护成本 + 失效风险);最典型的无效优化是「外层useCallback+ 内层箭头函数」(应该「直接传稳定函数」或「data-*+ 事件委托」);依赖里有字面量就等于没写(先修依赖);useMemo不是语义保证(React 可丢弃缓存,别放「只执行一次」的逻辑);useCallback(fn, deps)等价于useMemo(() => fn, deps);React Compiler 会减少手写,但不减少理解;判断标准永远是「Profiler 显示它是热点了吗」而不是「看起来该加」。



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