← 返回题目列表

useMemo 和 useCallback 有什么区别?什么时候该用?

高频 中等 第 13 / 27 题 更新于 2026/07/27
ReactuseMemouseCallback性能优化

简化版

useMemo 缓存计算结果,useCallback 缓存函数引用。它们主要用于避免昂贵计算重复执行,或配合 React.memo 减少子组件因引用变化而重新渲染。不要无脑使用,维护缓存本身也有成本。

详细版

useMemo

const filtered = useMemo(() => {
  return list.filter(item => item.active);
}, [list]);

useCallback

const onSelect = useCallback((id) => {
  setSelected(id);
}, []);

useCallback(fn, deps) 基本等价于 useMemo(() => fn, deps)

适合场景:

  • 计算确实昂贵。
  • 传给 memo 子组件的对象/函数需要稳定引用。
  • 作为 effect 依赖时需要避免无意义变化。

完整版教学

一、React 每次 render 都会重新执行函数组件

函数组件重新执行时,内部对象字面量、数组字面量、函数都会重新创建。即使内容一样,引用也不一样。

如果子组件被 React.memo 包裹,但 props 里传了每次都新建的函数或对象,memo 也可能失效。

二、useMemo 缓存值

useMemo 适合缓存计算结果,比如大数组过滤、排序、复杂格式化。

但如果计算很轻,使用 useMemo 反而增加心智负担。React 仍然要保存依赖、比较依赖、维护缓存,不是免费午餐。

三、useCallback 缓存函数引用

useCallback 不会让函数内部执行更快,它只是让函数引用在依赖不变时保持稳定。它常和 React.memo 一起使用:

const Child = memo(function Child({ onClick }) {});

如果没有 memo 子组件,也没有引用稳定需求,useCallback 往往没必要。

四、面试追问与工程落地

常见追问是“为什么用了 useCallback 组件还是重渲染”。原因可能是父组件重渲染本来就会执行子 JSX;子组件没有 memo;或者其他 props 仍然每次变化。优化要看整条渲染链。

还会问“依赖数组能不能少写”。不能为了稳定引用漏依赖。漏依赖会制造旧闭包,逻辑错误比多渲染更严重。需要稳定又要拿最新值时,可以考虑 ref 或重新设计状态边界。

工程里先用 Profiler 找瓶颈,再加 memo。过早到处 useMemo/useCallback,会让代码像长满护身符,难读还不一定快。

五、先算缓存是否可能回本

缓存不是零成本:每次 Render 仍要创建依赖数组、逐项用 Object.is 比较,并保留上次结果。可以用一个粗略模型判断是否值得:净收益 ≈ 省掉的计算次数 × 单次计算成本 - 依赖比较与维护成本

假设过滤 10000 条数据一次耗时 4 ms,父组件因无关状态重渲染 20 次,而 list 和 query 只变化 1 次;命中 useMemo 可把约 80 ms 的重复计算降到接近 4 ms。若计算只是两个数相加,缓存开销和阅读成本反而可能超过计算本身。

高收益:4 ms 计算 × 19 次可跳过 ≈ 76 ms
低收益:0.001 ms 计算 × 19 次,通常不值得增加缓存
判断依据:生产构建下的重复次数与实测耗时
工具缓存对象典型消费者不适合的理由
useMemo计算结果当前组件或对象 prop轻量计算不划算
useCallback函数引用memo 子组件、依赖项不会减少函数调用成本
memo组件输出复用机会父组件更新链props 经常变化难命中
React Compiler编译期自动缓存已启用 Compiler 的项目需要构建配置与规则兼容

六、缓存不是语义保证,并注意最新闭包

useCallback(fn, deps) 可理解为 useMemo(() => fn, deps),但稳定引用并不意味着函数读取永远最新的数据;依赖遗漏时,它会长期保留创建那轮的闭包。为了“让函数不变”而删依赖,是用正确性换一次可能不存在的优化。

React 可能在特定情况下丢弃 useMemo 缓存,因此业务正确性不能依赖“计算只执行一次”。启用 React Compiler 后,编译器可以自动应用大量等价 memoization;未启用项目仍应先用 Profiler 找热点,再围绕稳定依赖做局部优化。

const visible = useMemo(
  () => filterTodos(todos, query),
  [todos, query]
);

判断顺序:先证明计算或子渲染昂贵,再确认依赖多数时候稳定,最后添加缓存并复测;三个条件缺一时先保持代码简单。

七、常见误区与追问

  • 误区:useCallback 会让函数执行得更快。 它缓存的是函数引用,真正调用函数时的业务计算没有减少。
  • 误区:useMemo 能保证对象永久保持同一引用。 它是性能缓存而非语义契约,React 可以在特定场景丢弃缓存。
  • 误区:依赖越少缓存命中率越高,所以可以省略。 漏依赖会保留旧闭包或旧计算结果,属于正确性错误。
  • 追问:没有 memo 子组件时 useCallback 有用吗? 只有其他引用稳定需求,例如作为某个 Hook 依赖;否则通常没有收益。
  • 追问:怎样比较缓存成本和计算成本? 在生产构建用 Profiler 或 Performance 测量,而不是凭数组长度猜测。
  • 追问:React Compiler 会自动做什么? 启用后会分析组件和 Hooks,并自动应用等价的值与组件 memoization。
  • 追问:为什么对象依赖总在变化? 对象字面量每轮产生新引用,可缩小到原始值依赖或把对象创建移进计算内部。

八、加强记忆

useMemo 缓存“值”,useCallback 缓存“函数引用”。它们服务于昂贵计算和引用稳定,不是每个函数都要包。

依赖必须真实完整,缓存只为性能服务,业务正确性永远不能依赖缓存永久存在。