陌上人如玉
公子世无双

12.7 useMemo/useCallback 真有用吗

12.7 useMemo/useCallback 真有用吗

一句话:有用,但只在三个场景——「配合 React.memo 稳定 props 引用」「计算确实昂贵」「作为其它 Hook 的依赖」;其余情况是负担(比较依赖 + 保存缓存 + 维护成本),所以标准答案是「先测量,再优化」。

一、结论先行

问题:useMemo / useCallback 真有用吗?

一句话回答:
  「有用,但要看场景——它们的价值只在『稳定引用』与『避免昂贵计算』,
    滥用反而增加成本。所以判断标准是『是否真的减少了工作』,
    而不是『看起来更专业』。」

⭐ 三个真正有用的场景:
① 配合 React.memo 稳定 props(⚠️ 前提:子组件是 memo 的)
② 计算确实昂贵(大数组排序/过滤/聚合,> ~1ms)
③ 作为其它 Hook 的依赖(保持引用稳定,避免 Effect 反复执行)

⭐ 四个「是负担」的场景:
① 廉价计算(a + b、items.length)
② 常量(应该提到模块作用域)
③ 子组件不是 memo(稳定了也没用)
④ 传给原生 DOM 元素(原生元素不参与 memo)

二、核心机制:它们解决的是「引用」问题

// 问题根源:每次渲染都会新建「对象/数组/函数」
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <MemoChild
        options={{ page: 1 }}               // ⚠️ 每次渲染都是新对象
        items={[1, 2, 3]}                   // ⚠️ 新数组
        onClick={() => console.log('x')}    // ⚠️ 新函数
      />
    </>
  );
}
// → React.memo 的浅比较失败 → 子组件每次都重渲染
// ✅ 用 useMemo / useCallback 稳定引用
function Parent2() {
  const [count, setCount] = useState(0);

  const options = useMemo(() => ({ page: 1 }), []);
  const items = useMemo(() => [1, 2, 3], []);
  const handleClick = useCallback(() => console.log('x'), []);

  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <MemoChild options={options} items={items} onClick={handleClick} />
    </>
  );
}
// ✅ props 引用稳定 → React.memo 生效 → 子组件不再无谓重渲染
⭐ 一句话记住:
   useMemo 缓存「结果」,useCallback 缓存「函数」——
   它们的作用是「让引用稳定」,而不是「让代码变快」。
   (useCallback(fn, deps) 完全等价于 useMemo(() => fn, deps))

三、三个真正有用的场景(带证据)

场景一:配合 React.memo 稳定 props

const Row = React.memo(function Row({ item, onSelect }: RowProps) {
  console.log('Row 渲染', item.id);
  return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});

function List({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
  // ⭐ 稳定回调引用(否则每一行都会重渲染)
  const handleSelect = useCallback((id: string) => onSelect(id), [onSelect]);

  return (
    <ul>
      {items.map((item) => (
        <Row key={item.id} item={item} onSelect={handleSelect} />
      ))}
    </ul>
  );
}
⭐ 这是最常见的「真正有用」的场景:
  长列表 + memo 行组件 + 稳定回调 —— 直接决定滚动/更新的流畅度。

⚠️ 但要注意「两个前提」:
① 子组件确实是 React.memo 的(否则稳定了 props 也没用)
② 「item」本身也要引用稳定(如果每次 filter/map 出新数组,memo 依旧失效)

场景二:计算确实昂贵

// ✅ 值得:10 万条数据的过滤 + 排序
const visible = useMemo(() => {
  return hugeList
    .filter((it) => it.name.includes(keyword))
    .sort((a, b) => b.score - a.score);
}, [hugeList, keyword]);

// ✅ 值得:复杂聚合/统计
const stats = useMemo(() => computeStats(rawData), [rawData]);

// ✅ 值得:正则/日期格式化的批量处理
const formatted = useMemo(() => rows.map(formatRow), [rows]);
⭐ 怎么判断「昂贵」?
① 数据量(几千以上?有嵌套循环?)
② 复杂度(O(n log n) 或更高?)
③ **Profiler 证据**(它在渲染耗时里占显著比例吗?)
经验阈值:超过约 1ms 的计算值得考虑缓存。

场景三:作为其它 Hook 的依赖

// ✅ 值得:保持引用稳定,避免 Effect 反复执行(这是「正确性需要」)
const config = useMemo(() => ({ url, method, headers }), [url, method, headers]);

useEffect(() => {
  fetchData(config);
}, [config]);
// ⚠️ 如果不 memo:config 每次渲染都是新对象 → Effect 每次都执行 → 可能无限循环
⭐ 这一条很重要(容易被忽略):
   它不只是「性能优化」,而是「**修正 Effect 的语义**」——
   依赖数组的含义是「什么时候需要重新同步」,
   引用不稳定会让这个语义失效(Effect 每次都执行)。

四、四个「是负担」的场景

// ① 廉价计算(比较依赖的成本 > 计算成本)
const doubled = useMemo(() => count * 2, [count]);                  // ❌
const text = useMemo(() => String(value), [value]);                 // ❌

// ② 常量(应该提到模块作用域,那里的引用天然稳定)
const OPTIONS = useMemo(() => ({ a: 1, b: 2 }), []);                // ⚠️
const OPTIONS2 = { a: 1, b: 2 };                                    // ✅ 模块级常量

// ③ 子组件不是 memo(稳定了 props 也没意义)
function Parent() {
  const handleClick = useCallback(() => {}, []);                     // ⚠️ 白做
  return <Child onClick={handleClick} />;                            // Child 没 memo
}

// ④ 传给原生 DOM 元素(原生元素不是组件,不参与 props 比较)
const onClick = useCallback(() => {}, []);                           // ⚠️ 白做
return <button onClick={onClick} />;
⭐ 为什么「是负担」?—— 成本清单:
① 每次渲染都要「比较依赖数组」(逐个 Object.is)
② React 要保存「上一次的值 + 上一次的依赖」(内存)
③ 代码复杂度(依赖数组要维护正确)
④ 失效风险(写错 = 完全没用,还给人「已优化」的错觉)

五、最典型的「无效优化」

// ❌ 看起来做了优化,实际完全无效
function List({ items, onDelete }: Props) {
  const handleDelete = useCallback((id: string) => onDelete(id), [onDelete]);
  // ⭐ 函数引用确实稳定了

  return (
    <ul>
      {items.map((item) => (
        <Row
          key={item.id}
          item={item}
          onDelete={() => handleDelete(item.id)}     // ⚠️ 这里又创建了新函数!
        />
      ))}
    </ul>
  );
}
// → Row 收到的 onDelete 依然是「每次渲染都不同的函数」→ memo 失效
// ✅ 修复一:把「不需要参数的稳定函数」直接传下去
const Row = React.memo(function Row({ item, onDelete }: { item: Item; onDelete: (id: string) => void }) {
  return <li onClick={() => onDelete(item.id)}>{item.name}</li>;    // ⭐ 箭头函数在 Row 内部(不影响 Row 的 memo)
});

function List({ items, onDelete }: Props) {
  const handleDelete = useCallback((id: string) => onDelete(id), [onDelete]);

  return (
    <ul>
      {items.map((item) => (
        <Row key={item.id} item={item} onDelete={handleDelete} />   {/* ⭐ 直接传,不在外层包箭头 */}
      ))}
    </ul>
  );
}
// ✅ 修复二:data-* + 事件委托 → 处理函数永久稳定
function List2({ items, onDelete }: Props) {
  const onDeleteRef = useRef(onDelete);
  useEffect(() => { onDeleteRef.current = onDelete; });

  const handleClick = useCallback((e: React.MouseEvent<HTMLUListElement>) => {
    const id = (e.target as HTMLElement).closest('[data-id]')?.getAttribute('data-id');
    if (id) onDeleteRef.current(id);
  }, []);                                          // ⭐ 依赖为空 → 引用永久稳定

  return (
    <ul onClick={handleClick}>
      {items.map((item) => <RowWithoutCallback key={item.id} item={item} />)}
      {/*  ⭐ RowWithoutCallback 的 props 只有 item(稳定)→ memo 永久生效 */}
    </ul>
  );
}

六、依赖数组的三个常见错误

// ① 漏了依赖 → 结果过期(闭包陷阱)
const sorted = useMemo(() => [...list].sort(cmp), []);                  // ❌ 应该依赖 [list]

// ② 依赖里有「每次渲染新建的对象/函数」→ 完全失效
const value = useMemo(() => compute(a), [a, { extra: 1 }]);             // ❌ 每次都重算
const fn = useCallback(() => f(a), [{ a }]);                            // ❌ 同上

// ③ 依赖了「整个对象」但只用一个字段
useEffect(() => { doSomething(user); }, [user]);                        // ⚠️ user 变化频繁
useEffect(() => { doSomething(user.id); }, [user.id]);                  // ✅ 依赖原始值
⭐ 一个实用的检查清单(写完 useMemo 后自问):
① 依赖数组「是否包含计算里用到的所有响应式值」?(漏了 → 结果过期)
② 依赖里「有没有字面量对象/内联函数」?(有 → 完全失效)
③ 「下游是谁」?—— 是 memo 组件 / Effect?还是「原生元素 / 普通渲染」?
   · 前者 → 有价值
   · 后者 → 大概率没价值

七、useMemo 的三个「反直觉」事实

① 【不是语义保证】
   React 官方明确说:useMemo 是「性能优化手段,不是语义保证」——
   React 保留「在必要时丢弃缓存」的权利(如为了释放内存)。
   → 所以「只执行一次」的逻辑不要放在 useMemo 里
     (应该用 useState 惰性初始化 / useRef 延迟初始化 / useEffect(..., []))

② 【不保证「不重算」】
   因为「React 可能丢弃缓存」,所以「重算」在语义上是允许的。
   它保证的是「依赖不变时通常复用」——这是「尽力而为」。

③ 【依赖数组本身有成本】
   每次渲染都要「逐项比较」,字段极多时这个成本不可忽略。
   → 所以「依赖一个包含 20 个字段的对象」可能不如「依赖 2 个用到的字段」。

八、常见追问

Q: 那到底该不该「无脑加 useMemo」?
A: 不该。默认不写;只有当「Profiler 显示这里是热点」
   或「作为依赖」「配合 memo」时才加。无脑加的结果通常是
   「代码更复杂、性能没提升、还引入了隐蔽 bug(依赖写错)」。

Q: useMemo 能提升性能吗?
A: 分场景:
   ✅ 配合 memo / 昂贵计算 / 作为依赖 → 能
   ❌ 其它情况 → 通常不能甚至变负(比较成本 + 缓存成本)

Q: 加了 useMemo 却没效果,为什么?
A: 三个常见原因:
   ① 依赖里有「每次渲染新建的值」→ 每次都重算
   ② 子组件不是 memo → 稳定了 props 也没用
   ③ 子组件是 memo 但「还有别的 props 不稳定」→ 一个不稳定就全部失效

Q: useCallback 和 useMemo 的关系?
A: useCallback(fn, deps) === useMemo(() => fn, deps)。
   useCallback 只是 useMemo 的「语法糖」。

Q: React Compiler 会取代它们吗?
A: 会「大幅减少手写」,但不会完全取代——
   结构性问题(Context、children 透传、虚拟滚动)它管不了,
   而且它「跳过不纯的组件」。所以「理解引用稳定」依然必要。

Q: 为什么「传给原生元素」的 useCallback 没意义?
A: 因为 React.memo 只对「组件」生效;
   原生元素(<div>/<button>)不是组件,
   父组件渲染时它本来就会被重新创建并应用新属性。

Q: 在 useEffect 依赖里放 useCallback 的函数,一定对吗?
A: 只有当「这个函数确实需要稳定」时才对。
   如果「函数只在 Effect 内部用」,更好的做法是「把它移进 Effect」
   (依赖就变成函数用到的值,更简单也更不容易错)。

九、常见坑

// ① 无脑给所有回调加 useCallback(代码噪音大、收益为零)
// ② 依赖数组写错(漏了 → 过期;多了 → 失效)
// ③ 在 useMemo 里做副作用(应该在 Effect 里)
const v = useMemo(() => { track(); return compute(); }, []);          // ❌

// ④ 认为「useMemo 保证只算一次」
// ⑤ 把常量用 useMemo 包起来(应该提到组件外)
// ⑥ 子组件没 memo,父组件却写了一堆 useCallback
// ⑦ 「为了消除 eslint 警告」把函数包进 useCallback 但依赖写成 []
const h = useCallback(() => console.log(user), []);                    // ❌ 闭包陷阱
// ⑧ 在列表里「外层 useCallback + 内层箭头函数」(外层白做)
{items.map((it) => <Row onDelete={() => handleDelete(it.id)} />)}      // ❌

// ⑨ 层层 useMemo 导致可读性崩坏
// ⑩ 用「渲染次数」判断优化效果(应该看耗时)
// ⑪ 认为「加了 useMemo 就一定更快」(可能变慢)
// ⑫ 在 useMemo 的依赖里调用函数(每次渲染都执行)
useMemo(() => f(a), [getKey()]);                                       // ❌
// ⑬ 缓存了「大对象」却忘了「缓存本身占内存」
// ⑭ 把 useMemo 当「计算缓存」用于「跨组件的共享」(那是 Query/store 的活)

十、面试延伸

  1. 「useMemo / useCallback 真有用吗?」
    有用,但只在三个场景:① 配合 React.memo 稳定 props(让 memo 的浅比较能通过,从而跳过子组件重渲染);② 计算确实昂贵(大数组排序/过滤/聚合);③ 作为其它 Hook 的依赖(保持引用稳定,避免 useEffect 反复执行——这是「正确性」需要而非纯粹的性能优化)。其它情况通常是负担:廉价计算、常量(应提到模块作用域)、子组件不是 memo、传给原生 DOM 元素。

  2. 「无脑加 useMemo 有什么问题?」
    四个成本:① 依赖比较——每次渲染都要逐项 Object.is;② 缓存保存——React 要存「上一次的值与依赖」;③ 维护成本——依赖数组容易写错(漏了 → 结果过期;含字面量 → 完全失效);④ 误导性——写错的 useMemo 看起来「已经优化」,实际毫无作用(还掩盖了真正的瓶颈)。所以规范是「默认不写,需要时再加」。

  3. 「为什么『传给原生元素的 useCallback』没意义?」
    因为 React.memo 只对「组件」生效。原生元素(<div>、<button>)不是组件,不存在「props 浅比较跳过渲染」的机制——无论 onClick 的引用是否稳定,父组件渲染时这个元素都会被重新创建并应用新属性。所以这类回调直接写内联函数更清晰(除非它本身要作为别的 Hook 的依赖)。

  4. 「为什么『子组件不是 memo』时 useCallback 也白做?」
    因为「父组件重渲染时,子组件默认也会重渲染」(见 4.6)——这与「props 引用是否稳定」无关。React.memo 是唯一的「用 props 比较来跳过子组件渲染」的机制;没有它,父组件重渲染 → 子组件函数会被重新调用。所以在「没有 memo 的子组件」上稳定 props 是纯粹的浪费(还增加了依赖维护成本)。

  5. 「为什么说『作为依赖』是用它的正当理由?」
    因为 useEffect(fn, [dep]) 里的 dep 若是「每次渲染都新建的对象/数组/函数」,会导致 Effect 每次都执行(甚至无限循环)。这不是「性能问题」而是「语义问题」——依赖数组的含义是「什么时候需要重新同步」,而引用不稳定会让这个语义失效。所以「为了稳定依赖而 memo」是在修正语义,属于必须做的事(而不是可选的优化)。

  6. 「useMemo 能保证计算只执行一次吗?」
    不能。React 官方明确说明「useMemo 是性能优化手段,不是语义保证」——React 保留「在必要时丢弃缓存」的权利(如为了释放内存)。所以不要把「只该执行一次」的逻辑放在 useMemo 里(如初始化第三方库、发请求、注册监听)。需要「只执行一次」应该用:useState 的惰性初始化、useRef 的延迟初始化,或 useEffect(() => {...}, [])。

  7. 「加了 useMemo 却没效果,怎么排查?」
    按顺序查三点:① 依赖数组——里面有没有「每次渲染新建的值」(字面量对象、内联函数、filter 出的新数组)?有的话把它稳定掉(提到模块外 / useMemo / 依赖原始值);② 下游是不是 memo 组件——如果不是,useMemo 白做;③ 下游是 memo 了,但它还有别的 props 不稳定——只要一个 props 引用变了,浅比较就失败,其余 memo 都是白做。排查工具是 Profiler 的「Why did this render?」(它会告诉你哪个 prop 变了)。

  8. 「React Compiler 之后还需要手写吗?」
    手写频率大幅下降,但理解仍然必要。因为:① Compiler 会自动插入记忆化(比手写更精确、更少出错);② 但它只覆盖「局部记忆化」——结构性问题(Context 的订阅粒度、children 透传、状态下移、虚拟滚动、代码分割)它管不了;③ 它跳过「不纯的组件」(你要能识别哪些代码不纯);④ 跨组件的引用传递仍需理解(否则看不懂「为什么加了 memo 还是重渲染」)。一句话:Compiler 减少机械劳动,不减少理解成本。

一句话速记

useMemo/useCallback 只在三个场景有用:「配合 React.memo 稳定 props(前提:子组件是 memo 的)」「计算确实昂贵(> ~1ms,Profiler 验证)」「作为其它 Hook 的依赖(语义需要)」;其余是负担(依赖比较 + 缓存 + 维护成本 + 失效风险);最典型的无效优化是「外层 useCallback + 内层箭头函数」(应该「直接传稳定函数」或「data-* + 事件委托」);依赖里有字面量就等于没写(先修依赖);useMemo 不是语义保证(React 可丢弃缓存,别放「只执行一次」的逻辑);useCallback(fn, deps) 等价于 useMemo(() => fn, deps);React Compiler 会减少手写,但不减少理解;判断标准永远是「Profiler 显示它是热点了吗」而不是「看起来该加」。

赞(0) 打赏
未经允许不得转载:陌上寒 » 12.7 useMemo/useCallback 真有用吗

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫