useMemo 和 useCallback 有什么区别?什么时候不该用?
useMemo 调用你传的函数并缓存它的返回值,useCallback 不调用函数,直接缓存函数本身,useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。只有计算明显很慢、要传给 memo 组件或者要当其他 Hook 的依赖时才有用;启用 React Compiler 后,新代码一般交给编译器,手写的只作为精确控制的手段。
简答
两个 Hook 都是“依赖没变就返回上次的东西”,区别在于缓存的是什么。
useMemo(() => filter(items), [items])会调用这个函数,缓存的是它返回的结果。useCallback(handleClick, [id])不调用函数,缓存的是函数本身。所以useCallback(fn, deps)就等于useMemo(() => fn, deps)。为什么需要缓存函数?因为组件每次渲染,里面写的函数都会重新创建,引用每次都不同。这个函数如果传给了用
memo包裹的子组件,或者写进了useEffect的依赖,引用一变,子组件就重新渲染,effect 就重新执行。官方文档列出的适用场景很窄:
useMemo适合计算明显很慢且依赖很少变化、要传给memo组件、要作为其他 Hook 依赖这三种情况;useCallback只适合后两种。其他情况加了没有好处,只会让代码更难读。有了 React Compiler 以后,官方建议新代码依赖编译器自动缓存,需要精确控制时才手写;已有代码里的
useMemo、useCallback先保留,删除前要仔细测试,因为删掉可能改变编译结果。另外它们只是性能优化,React 可能在某些情况下丢弃缓存,程序逻辑不能依赖它们。
一个缓存结果,一个缓存函数
下面用一个极简的模拟看两者的行为。它按调用顺序保存上一次的值和依赖,依赖用 Object.is 逐个比较,和 React 的规则一致。真实的 React 实现要复杂得多,这里只用来说明效果。
function createHooks() {
const slots = [];
let index = 0;
const depsChanged = (prev, next) => !prev || next.some((dep, i) => !Object.is(dep, prev[i]));
function useMemo(calculate, deps) {
const slot = slots[index++];
if (slot && !depsChanged(slot.deps, deps)) return slot.value;
const value = calculate();
slots[index - 1] = { value, deps };
return value;
}
const useCallback = (fn, deps) => useMemo(() => fn, deps);
const render = (component, props) => {
index = 0;
return component(props);
};
return { useMemo, useCallback, render };
}
const { useMemo, useCallback, render } = createHooks();
let calls = 0;
function List({ items, query }) {
const visible = useMemo(() => {
calls += 1;
return items.filter((item) => item.includes(query));
}, [items, query]);
const onSelect = useCallback((id) => id, [query]);
return { visible, onSelect };
}
const items = ['apple', 'banana', 'cherry'];
const first = render(List, { items, query: 'a' });
const second = render(List, { items, query: 'a' });
const third = render(List, { items, query: 'e' });
console.log(calls); // 2
console.log(first.visible === second.visible); // true
console.log(first.onSelect === second.onSelect); // true
console.log(second.onSelect === third.onSelect); // false
console.log(third.visible.join(',')); // apple,cherry
渲染了三次,过滤只执行了两次。第二次渲染时 items 和 query 都没变,useMemo 直接返回上次的数组,所以 first.visible === second.visible。第三次 query 变了,才重新过滤。
useCallback 这边,每次渲染其实都新建了一个箭头函数,这一点它阻止不了。它做的是:依赖没变时丢掉新建的那个,把上次的函数还给你。官方文档原话的意思也是这样:你总是在创建函数,只是 React 忽略它,返回缓存的那个。
什么时候不该用
官方文档给的判断标准是看场景,不是看“能不能加”。计算本身不慢,比如几十个元素的 filter,缓存省下的时间还不如比较依赖的开销显眼。文档建议先用 console.time 测一下,累计耗时达到 1 毫秒左右或更多,才考虑 useMemo。测量时要用生产构建,因为开发环境的严格模式会让组件渲染两次,数字不准。
useCallback 更是如此。函数没有传给 memo 组件,也没有被别的 Hook 当依赖,缓存它没有任何用处,因为没人在比较它的引用。
缓存也很容易失效。只要传给 memo 组件的 props 里有一个每次都是新对象,整个组件的缓存就白做了。所以官方更推荐先调整结构:把包裹其他组件的组件改成接收 children,把状态放在真正用到它的地方,去掉不必要的 effect 依赖,比如把对象或函数挪进 effect 内部或组件外部。这些做完,很多缓存就不需要了。
还有一类误用是靠缓存来保证正确性。比如“不缓存这个对象,effect 就会无限执行”。文档的说法是,代码离开 useMemo 就不能工作,应该先找到并修好根本问题。React 也明确说过可能丢弃缓存,比如开发时编辑组件文件,或者组件在首次挂载时挂起。
React Compiler 现在是什么情况
React 官方文档说明,React Compiler 已经稳定,在 Meta 等公司的生产环境中使用,支持 React 17、18 和 19,可以通过 Babel、Vite、Metro、Rsbuild 等构建工具接入。它在构建时分析遵守 React 规则的组件和 Hook,自动加上等价于手写 useMemo、useCallback、memo 的缓存。
关于手写的缓存,官方的建议分新旧代码。新代码依赖编译器,需要精确控制时再用 useMemo、useCallback,典型场景是某个值被当作 effect 的依赖,要保证它没有实质变化时 effect 不重复执行。已有代码可以保留原来的缓存,删除前要仔细测试,因为删掉可能改变编译输出。
编译器也有边界。它只缓存组件和 Hook 里的东西,不会缓存组件外部的普通函数;它的缓存也不会在多个组件之间共享。所以一个昂贵的计算被很多组件调用时,官方建议考虑在 React 之外单独做缓存。更多细节见《React Compiler 解决了哪些优化问题?》。
常见追问
useMemo 和 memo 是一回事吗?
不是。
useMemo是 Hook,在组件内部缓存一个值;memo包裹整个组件,props 没变时跳过它的渲染。两者经常配合使用,见《memo、useMemo、useCallback 各自优化什么?》。
依赖数组写漏了会怎样?
缓存不会在该更新时更新,函数里读到的还是旧的 props 或 state。文档建议配置 React 的 lint 规则,它会检查每个用到的响应式值是否都写进了依赖。
useMemo 里能发请求或者改外部变量吗?
不应该。传给
useMemo的计算函数必须是纯函数,严格模式下开发环境会故意调用它两次来帮你发现副作用。请求这类副作用应该放在事件处理函数或 effect 里。