
一句话:两者都是「引用稳定工具」——
useMemo缓存计算结果,useCallback缓存函数引用;它们的价值只在「配合React.memo」或「计算确实昂贵」时才成立,滥用反而增加成本。
一、基本用法
// useMemo:缓存「计算的结果」
const sorted = useMemo(() => {
return [...list].sort((a, b) => a.order - b.order); // 只有 list 变化时才重算
}, [list]);
// useCallback:缓存「函数本身」(等价于 useMemo 返回函数)
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
// ⭐ 两者的关系:useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
const handleClick2 = useMemo(() => () => doSomething(id), [id]);
| Hook | 缓存对象 | 返回值 | 使用场景 |
|---|---|---|---|
useMemo |
计算结果(任意值) | 计算结果 | 昂贵计算、稳定对象/数组引用 |
useCallback |
函数本身 | 函数 | 稳定函数引用(给 memo 子组件 / 作为依赖) |
二、两者解决的核心问题:引用稳定
// 🎯 问题:每次渲染都是新的引用
function Parent() {
const [count, setCount] = useState(0);
// 每次渲染都会新建:新的对象、新的数组、新的函数
const options = { page: 1, size: 10 };
const list = [1, 2, 3];
const handleClick = () => console.log('click');
return (
<>
{/* ⚠️ 三个 props 全是新引用 → React.memo 的浅比较全部失败 → 子组件每次都重渲染 */}
>
);
}
const MemoChild = React.memo(function Child({ options, list, onClick }: Props) {
console.log('Child 渲染');
return {list.length};
});
// ✅ 稳定引用
function Parent2() {
const [count, setCount] = useState(0);
const options = useMemo(() => ({ page: 1, size: 10 }), []); // 常量可提到模块作用域
const list = useMemo(() => [1, 2, 3], []);
const handleClick = useCallback(() => console.log('click'), []);
return (
<>
{/* ✅ props 引用稳定 → React.memo 生效 → 子组件不再无谓重渲染 */}
>
);
}
注意:如果
options是纯常量(不依赖任何值),其实应该提到组件外部(模块作用域),这样根本不需要useMemo:const OPTIONS = { page: 1, size: 10 }; // ✅ 模块级常量,引用天然稳定
三、什么时候真正有用(三个场景)
// ① 配合 React.memo 稳定 props 引用
const Child = React.memo(ExpensiveChild);
function Parent({ items, onSelect }) {
const sorted = useMemo(() => [...items].sort(), [items]);
const handleSelect = useCallback((id) => onSelect(id), [onSelect]);
return ;
}
// ② 计算确实昂贵(避免每次渲染重算)
const stats = useMemo(() => {
return expensiveAggregate(hugeList); // 例:10 万条数据的统计
}, [hugeList]);
// ③ 作为其它 Hook 的依赖(保持引用稳定,避免 Effect 反复执行)
const config = useMemo(() => ({ url, method }), [url, method]);
useEffect(() => {
fetchData(config);
}, [config]); // ✅ config 引用稳定 → 只有 url/method 变化时才重新请求
// ⚠️ 如果不 memo,config 每次渲染都是新对象 → Effect 每次都执行 → 可能无限循环
| 场景 | 该不该用 |
|---|---|
| 值是常量(不依赖任何响应式值) | ❌ 提到模块作用域更好 |
计算非常轻(a + b、简单 map) |
❌ 不值得 |
结果要给 memo 子组件作 props |
✅ 该用 |
| 计算昂贵(大数组排序/过滤/聚合) | ✅ 该用 |
作为 useEffect/useMemo 的依赖 |
✅ 该用 |
一次性的简单函数给原生元素(<button onClick>) |
❌ 不需要 |
useMemo 里做副作用 |
❌ 不应该(应放 Effect) |
四、成本分析:为什么可能是负担
useMemo / useCallback 并不是「免费的」:
① 内存成本:React 要保存上一次的值与依赖数组
② 执行成本:每次渲染都要【比较依赖数组】(逐项 Object.is)
③ 代码成本:依赖数组要维护正确(漏了会出错,多了会失效)
④ 可读成本:包一层后逻辑变长,尤其嵌套时
⑤ 失效成本:写错的 useMemo 会「看起来在优化」,实际毫无作用(props 依然不稳定)
所以「无脑加 useMemo/useCallback」的结果常常是:
代码更复杂、性能没提升、还引入了隐蔽 bug。
// ❌ 典型的「无效优化」
function Bad({ items }) {
// 计算很轻,包一层反而更贵
const count = useMemo(() => items.length, [items]);
// 传给原生元素,不需要稳定引用(原生元素不是 memo 组件)
const onClick = useCallback(() => console.log('ok'), []);
return ;
}
// ✅ 直接写就好
function Good({ items }) {
return ;
}
五、常见误用
// ① 依赖数组漏了 → 结果是「过期的」
const sorted = useMemo(() => [...list].sort(cmp), []); // ❌ 应该依赖 [list]
// → 表现:list 变化了但 sorted 不变
// ② 依赖数组多写了「每次渲染都新建的东西」→ 完全失效
const value = useMemo(() => compute(a), [a, { extra: 1 }]); // ❌ 第二个依赖每次都是新对象
// → 表现:每次都重新计算(等于没 memo)
// ③ 用 useMemo 缓存「本来就不稳定」的结果
const fn = useMemo(() => (x) => x + a, [a]); // ✅ 这个是对的
const obj = useMemo(() => ({ a, b }), [a, b]); // ✅ 对
// ④ 在 useMemo 里做副作用(不应该)
const data = useMemo(() => {
localStorage.setItem('x', 'y'); // ❌ 副作用不该在 useMemo 里
return compute();
}, []);
// ⑤ useCallback 包裹的函数内部依赖了未声明的值
const handle = useCallback(() => {
console.log(user.name); // ⚠️ user 没在依赖里 → 拿到旧值
}, []);
// ⑥ 给「每次渲染都要更新的值」做 memo → 依赖总是变化 → 无效
const dynamic = useMemo(() => ({ now: Date.now() }), [Date.now()]); // ❌ 每次都变
// ⑦ 用 useMemo 替代「状态派生」的正确写法(其实应该直接算)
// 如果计算很轻,直接 const x = derive(a, b) 就行
// ⑦ 的示例:轻量派生不需要 useMemo
function Bad({ firstName, lastName }) {
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// ❌ 字符串拼接极快,memo 的成本 > 收益
return {fullName};
}
function Good({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`; // ✅ 直接算
return {fullName};
}
六、依赖数组的写法规范
// ① 依赖「用到的每一个响应式值」
const value = useMemo(() => compute(a, b), [a, b]);
// ② props 对象依赖「具体字段」
const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), [items]);
// 如果父组件能保证 items 引用稳定,就依赖 items;
// 否则依赖「能稳定比对的原始值」(如 items.length、items.map(i => i.id).join())
// ③ 函数依赖(层层下钻会很麻烦)
const handle = useCallback(() => onSelect(id), [onSelect, id]);
// ④ 需要「稳定但读最新值」时结合 useRef
const latestOnSelect = useRef(onSelect);
useEffect(() => { latestOnSelect.current = onSelect; });
const handleStable = useCallback((id: string) => {
latestOnSelect.current(id); // 引用稳定,但调用的是最新的 onSelect
}, []); // ⭐ 依赖为空,引用永不变化
七、与 React.memo 的关系
三者的关系(必须一起理解):
React.memo → 浅比较 props,相同则跳过子组件重渲染
↑
│ 需要 props 引用稳定
│
useMemo → 稳定「值」的引用(对象/数组/计算结果)
useCallback → 稳定「函数」的引用
所以:
· 只用 React.memo,不配合 useMemo/useCallback → 大概率失效(props 每次都是新引用)
· 只用 useMemo/useCallback,子组件不是 memo → 没有意义(子组件本来就每次渲染)
· 三者配合才有意义,但也不能无脑加 → 判断标准是「渲染成本是否真的高」
// 完整的配合示例
const Row = React.memo(function Row({ item, onDelete }: RowProps) {
console.log('Row 渲染', item.id);
return (
{item.name}
{/* ⚠️ 这里的箭头函数是「Row 内部的」,不影响 Row 自身的 memo */}
);
});
function List({ items, onDelete }: { items: Item[]; onDelete: (id: string) => void }) {
// 稳定的处理函数(如果 onDelete 本身稳定)
const handleDelete = useCallback((id: string) => onDelete(id), [onDelete]);
return (
{items.map((it) => (
))}
);
}
// ✅ 只要 item 与 handleDelete 引用稳定,Row 就不会重渲染
八、常见坑
// ① 用 useMemo 包「廉价计算」
const doubled = useMemo(() => count * 2, [count]); // ❌ 不值得
// ② 依赖数组漏值 → 结果过期
const filtered = useMemo(() => list.filter(fn), []); // ❌
// ③ 依赖数组里有新对象 → 完全失效
const v = useMemo(() => f(a), [a, {}]); // ❌
// ④ useCallback 里用了未声明的依赖
const h = useCallback(() => g(user), []); // ❌ user 会是旧值
// ⑤ 在 useMemo 里写副作用
useMemo(() => { track(); return 1; }, []); // ❌
// ⑥ 给「常量」做 memo(应该提到组件外)
const OPTIONS = useMemo(() => ({ a: 1 }), []); // ⚠️ 模块级常量更好
// ⑦ 把所有回调都 useCallback → 代码噪音大,收益为零
// ⑧ 「为了消除 eslint 警告」而把函数用 useCallback 包起来,但依赖写错
// ⑨ 认为 useMemo 能「保证」不重算(React 保留丢弃缓存的权利)
// ⑩ 在 useMemo 的依赖里用「引用会变但内容相同」的对象 → 无效 memo
// ⑪ 用 useMemo 记忆「带副作用的方法调用」
// ⑫ 忘记「子组件不是 memo 时,父组件的 useCallback 毫无意义」
面试延伸
- 「
useMemo和useCallback有什么区别?」
两者都是「引用稳定工具」,区别在缓存的对象不同:useMemo缓存计算结果(任意值),useCallback缓存函数本身。它们本质上是同一个机制——useCallback(fn, deps)完全等价于useMemo(() => fn, deps),所以useCallback只是useMemo的语法糖。 -
「
useMemo/useCallback真的能优化性能吗?」
分场景:① 配合React.memo—— 能(稳定 props 引用让 memo 生效,跳过子组件重渲染);② 昂贵计算 —— 能(避免每次渲染重算);③ 作为其它 Hook 的依赖 —— 能(避免 Effect 反复执行);④ 其它情况 —— 通常不能甚至变负(比较依赖、保存缓存、维护依赖数组都有成本)。所以标准做法是「先用 Profiler 确认这里是瓶颈,再加」。 -
「为什么加了
useMemo却没有效果?」
三个最常见原因:① 依赖数组里有「每次渲染都新建的值」(对象/数组/内联函数字面量)→ 依赖永远「变了」→ 每次都重算;② 子组件不是React.memo包裹的 → 父组件重渲染时子组件本来就会重新执行,props 稳定与否没有意义;③ 子组件是 memo,但还有别的 props 不稳定 → 只要有一个 props 引用变化,浅比较就失败,其余 memo 都白做。 -
「
useMemo能保证计算只执行一次吗?」
不能。React 官方文档明确说「useMemo是性能优化手段,不是语义保证」——React 保留在必要时丢弃缓存的权利(例如为了释放内存、或在未来的并发特性下)。所以不要把「只执行一次」的逻辑放在useMemo里(如初始化只该执行一次的副作用),那种场景应该用useRef的延迟初始化或useState的惰性初始化。 -
「什么时候不该用
useMemo/useCallback?」
① 计算很轻(简单算术、字符串拼接、小数组map);② 常量(应该提到模块作用域);③ 传给原生 DOM 元素的事件(原生元素不是 memo 组件,不需要稳定引用);④ 子组件不是memo的;⑤ 为了「消除 lint 警告」而机械地包一层(这会让依赖维护变复杂)。判断标准是「是否真的减少了工作」。 -
「
useCallback的依赖怎么写才不会一直变?」
常见问题:依赖里有「父组件传下来的内联函数」或「每次渲染新建的对象」,导致useCallback的引用也一直变。解法:① 让上游也稳定(层层useCallback/useMemo);② 依赖「原始值」而不是对象;③ 需要「稳定但读最新值」时用useRef保存最新值 +useCallback(..., [])(引用永不变化);④ 把处理函数做成「用data-*传参」的稳定函数。 -
「
useMemo和useCallback与 React Compiler 的关系?」
React Compiler(React 19 的实验特性)会自动在需要的地方插入记忆化(相当于自动帮你写useMemo/useCallback/memo),并且比手写更精确。所以长期看「手动优化」的价值会下降。但目前:① Compiler 还未全面落地;② 手动优化的思路(理解引用稳定、减少重渲染)依然重要——因为 Compiler 也有覆盖不到的地方(如跨组件的引用传递),而且「知道什么时候该优化」是性能工作的基础。 -
「如何判断一个
useMemo是否值得保留?」
三个检查项:① 依赖数组是否稳定(如果依赖里每次都是新引用,它完全没作用,应该先修依赖或删掉它);② 计算结果是否被memo子组件或其它 Hook 使用(如果只是给原生元素或只用于当前组件的渲染,通常不值得);③ 计算是否真的昂贵(用 Profiler 看该函数的耗时占比,占比很低就不值得)。不符合以上三条的useMemo应该删掉——删除它们往往能让代码更简单且性能不降。
一句话速记
useMemo缓存结果、useCallback缓存函数(后者是前者的语法糖);它们的本质是「引用稳定」,价值只在「配合React.memo」「计算昂贵」「作为其它 Hook 的依赖」时成立;依赖里有新对象/新函数会完全失效(先修依赖);常量的 memo 应该提到模块作用域、轻量计算不值得 memo;useMemo不是语义保证(React 可丢弃缓存),别把「只执行一次」的逻辑放进去。



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