跳到主要内容

useMemo 用太多了:先量出 1 毫秒,再决定要不要包,顺手把 React Hooks 速查表放在手边

用 Node 实测数据说话:10,000 项 filter+sort 只要 0.193ms,远低于 React 官方建议的 1ms 记忆化阈值。讲清 useMemo / useCallback 什么时候真有用、什么时候纯属仪式感。

发布于

useMemo 用太多了:先量出 1 毫秒,再决定要不要包

打开任何一个跑了两年以上的 React 项目,搜一下 useMemo,大概率会看到这样的代码:把一个 50 项数组的 filter 包进 useMemo,把一个只在 JSX 里用了一次的箭头函数包进 useCallback,依赖数组写了五个变量,其中两个每次渲染都是新引用,所以记忆化从来没生效过,但代码已经比不包的版本长了一倍。这篇文章想说服你做一件事:在写 useMemo 之前,先打开控制台量一次。

React 官方给过一个具体数字:1 毫秒

很多人不知道,react.dev 的 useMemo 文档里其实写了一个可操作的阈值:用 console.time 把计算包起来量一下,如果总耗时达到了"显著的量级,比如 1ms 或更多",才值得考虑记忆化(见 react.dev useMemo 页 "How to tell if a calculation is expensive" 一节)。注意官方的措辞是"可能值得"(might make sense),不是"应该"。

1ms 是什么概念?浏览器一帧的预算是 16.7ms。一个低于 1ms 的计算,就算每次渲染都重跑,在性能面板里也基本看不见。而 useMemo 本身不是免费的:React 要存上一次的依赖数组和结果,每次渲染逐项 Object.is 比较,组件初次挂载时还多一次分配。给 0.001ms 的计算套记忆化,是在用真实的开销换取不存在的收益。

实测:10,000 项 filter + sort,到底多少毫秒

我在写这篇文章前,用 Node 20(和浏览器同款 V8 引擎)实测了一个典型的"派生列表"场景:商品列表按关键词过滤再按价格排序,每种规模跑 1,000 次取平均。输入是这样构造的:

const items = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: 'item-' + i,
  price: (i * 7919) % 1000,
}));

function derive(query) {
  return items
    .filter(it => it.name.includes(query))
    .sort((a, b) => a.price - b.price);
}

实际输出:

10000 项 filter+sort 平均耗时(ms): 0.193
50 项 filter+sort 平均耗时(ms): 0.0010
匹配条数: 11  前3项: [{"id":999,"name":"item-999","price":81}, …]

一万项的真实业务数据,过滤加排序只花 0.193ms,离官方的 1ms 阈值还有五倍余量。五十项的小列表是 0.001ms,记忆化它约等于给一次加法上缓存。换句话说:除非你的列表上了十万项,或者每项的计算本身很重(正则、深拷贝、Fuse.js 这类模糊匹配),否则裸写在渲染体里就是正确答案。

useCallback 真正必要的只有两类场景

useCallback 的误用率比 useMemo 还高,因为它的收益更间接:它本身不省任何计算,省的是"下游因为函数引用变了而白渲染"。这意味着它只在两类场景有意义:

  1. 函数传给了一个被 React.memo 包住的子组件。没有 memo,子组件本来就会跟着父级重渲,函数引用稳不稳定无所谓。
  2. 函数出现在别的 Hook 的依赖数组里,比如 useEffect(() => { … }, [onSave])。不稳定的引用会让 effect 每次渲染都重跑。

onClick={() => setOpen(true)} 包进 useCallback 再传给一个普通 <button>?DOM 元素不做引用比较,这层包装从第一天起就没有生效过。我在做 code review 时的习惯是反过来问:删掉这个 useCallback,哪个 memo 组件或哪个依赖数组会受影响?答不上来,就删。

"什么时候别用它"才是最难记的部分

每个 Hook 的签名看一眼文档就会,难的是反模式,而反模式恰恰散落在官方文档的各个角落。这也是我们把 React Hooks 速查表做成现在这个样子的原因:17 个内置 Hook 加 28 个高频自定义 Hook,每张卡片除了签名和真实示例,都有一栏"什么时候别用它"。useMemo 那张卡片写的就是本文的浓缩版:先量,低于 1ms 别包;useCallback 那张写的是上面那两条判定规则。按名字即时过滤,review 代码时开在旁边比翻 react.dev 快得多。

另外提一句正在改变这个话题的事:React Compiler(随 React 19 生态推进)会在编译期自动插入记忆化,目标就是让手写 useMemo / useCallback 成为历史。但在它覆盖你的项目之前,"先测量、再记忆化"仍然是唯一靠谱的纪律。

顺手清掉另一种仪式感代码

过度记忆化之外,React 项目里另一类常见的体力活是把设计稿或老页面的 HTML 手改成 JSX:classclassName、内联 style 字符串改对象、<img> 补自闭合。这种活没有任何思考含量,却很容易改漏。可以直接用 HTML 转 JSX 工具粘贴转换,全程在浏览器本地完成,不上传任何代码。

回到主题,记三句话就够了:useMemo 之前先 console.time,低于 1ms 不包;useCallback 只配合 memo 子组件或依赖数组使用;拿不准某个 Hook 的边界时,查速查表的"什么时候别用它"一栏,而不是凭印象。


Made by Toolora · Updated 2026-06-12