如果你做过 React 代码评审,大概率见过这种场面:
一个按钮点击函数,外面包了 useCallback;一个随手拼出来的对象,外面包了 useMemo;代码看起来很努力,但你问一句"为什么要包?",很多人其实答不上来。
Josh Comeau 那篇《Understanding useMemo and useCallback》把这件事讲得很透。
看完我最大的感受是:这两个 Hook 不是性能护身符,它们只是 React 里一套“记住引用”的机制。很多时候,真正该优化的不是记忆化,而是结构。
先搞懂一件事:React 默认就是重新执行
React 的工作方式,本来就是重新渲染。
只要状态变了,组件函数就会再执行一遍,重新生成一张 UI 快照。这个过程本身并不邪恶,也通常没有你想象中那么慢。
问题在于:JavaScript 里的对象、数组、函数,比较的是引用,不是内容。
也就是说:
• [] !== []
• {} !== {}
• () => {} !== () => {}
所以父组件每次重新执行,只要你顺手创建了一个新对象、新数组、新函数,哪怕内容一样,它们在 React 看来也是“新东西”。这就可能让 React.memo 失效,导致本来可以跳过的子组件又渲染了一次。
useMemo 和 useCallback,本质上在做什么?
Josh Comeau 的解释我觉得特别好:它们本质上是在多次渲染之间,通过闭包帮你记住上一次的值。
依赖没变,就把旧值拿出来继续用;依赖变了,才重新算一遍。
useMemo
它记住的是一个计算结果。
常见有两种用途:
缓存昂贵计算
比如排序大列表、过滤大量数据、复杂派生值计算。
保持引用稳定
比如你要给一个被 React.memo 包裹的子组件传对象或数组,那就需要让这个对象或数组在依赖不变时,保持同一个引用。
useCallback
它记住的是一个函数引用。
但它并不神秘,本质上只是 useMemo 的语法糖:
const handleClick = useCallback(fn, [deps]);
// 基本等价于
const handleClick = useMemo(() => fn, [deps]);
所以别把它想得太复杂。它不是“让函数更快”,而是“让函数别每次都换身份证”。
真正容易误解的地方
很多人一看到组件重新渲染,就条件反射地想:加 useMemo,加 useCallback。
但 Josh Comeau 反复强调一件事:不要过早优化。
因为你加的每一个记忆化,都是有成本的:
• 代码更难读
• 依赖数组更容易写错
• 心智负担更重
• 有时候收益几乎为零
换句话说,记忆化不是免费午餐。 如果没有真实的性能问题,只是为了“看起来专业”而到处包 Hook,大概率是在给未来的自己挖坑。
比起记忆化,更重要的是“结构优于记忆”
这篇文章里我最认同的,不是某个 API 细节,而是这个架构思想:
优先优化结构,其次才是记忆化。
什么意思?就是在你准备加 useMemo / useCallback 之前,先问自己:这个问题能不能通过拆组件、下放状态、调整边界来解决?
方法一:把状态下放
如果父组件里挂了很多互不相干的状态,那么任何一个状态变化,都会带着整棵子树一起重新执行。
这时候,最好的办法通常不是记忆化,而是把状态下放到真正使用它的那个子组件里。
谁需要这个状态,谁拥有它。
方法二:把重内容往外提
有些组件每次都被迫跟着父组件一起跑,不是因为它真依赖父组件的数据,而是因为它被写在里面。
这种时候,把内容提出来,或者通过 children 传进去,往往比加一堆 useMemo 更干净。
方法三:先想组件边界,再想 Hook
有时候真正该做的,是把某一块独立逻辑拆成子组件,再用 React.memo 把边界守住。
你缓存整个组件,比在组件内部零零散散缓存三个对象、两个函数,通常更容易维护。
这就是“结构优于记忆”的意思:先把房子盖对,再考虑要不要装节能玻璃。
那什么时候值得用?
可以记住一个很实用的判断顺序:
该用 useMemo 的情况
• 这段计算真的很重
• 它会因为无关状态频繁重复执行
• 或者你需要稳定的对象 / 数组引用,来配合 React.memo
该用 useCallback 的情况
• 你要把函数传给被 React.memo 包裹的子组件
• 你确实需要稳定的函数引用
• 否则它每次变,子组件优化就白做了
可以提前做优化的两个地方
Josh Comeau 提到两个例外,我觉得很实用:
通用自定义 Hook
你不知道别人会怎么用它,所以把返回函数用 useCallback 稳住,通常是值得的。
Context Provider
如果 value 每次都是新对象,下面所有消费者都可能被迫刷新。这里用 useMemo 往往很划算。
最后给自己留一个判断清单
以后每次想写 useMemo 或 useCallback,先问自己 3 个问题:
我是在解决真实性能问题,还是只是害怕重渲染?
这个问题能不能通过调整组件结构解决?
如果我加了它,收益真的大于复杂度吗?
如果这三关过不去,先别加。
总结
Josh Comeau 这篇文章最值得记住的,不是 API 用法,而是一个更底层的认知:
useMemo 缓存的是值,useCallback 缓存的是函数,但真正该优先缓存的,往往是你的架构判断力。
别把它们当成 React 性能优化的默认配置。
先改结构,再谈记忆。 这句话,我觉得比背十遍 Hook 用法都有用。
参考原文:Josh Comeau, Understanding useMemo and useCallback