useMemo,真的该到处用吗?

0 阅读6分钟

如果你做过 React 代码评审,大概率见过这种场面:一个组件里密密麻麻十几个 useMemo,连 !data.length 这种一步到位的布尔判断都要包一层。

问写的人为什么,答案通常是:"性能优化嘛,包一下更稳。"

但 useMemo 不是免费的保险——它自己也有成本。

一、useMemo 到底在干什么

一句话:在重新渲染之间,缓存上一次的计算结果,如果依赖没变就直接复用。

const visibleTodos = useMemo(
  () => filterTodos(todos, tab),
  [todos, tab]
);

首次渲染时,React 执行 filterTodos(todos, tab) 并记住结果。后续每次渲染,React 用 Object.is 逐个比较 todos 和 tab 是否和上次一样——都没变就直接返回缓存值,跳过计算。

注意两个关键词: "缓存"和"比较" ——这两件事本身都不免费。

二、缓存也有成本——经济学的机会成本视角

经济学有一条铁律:每个决策都有机会成本。  选择缓存一个值,你同时付出了三项成本:

成本类型具体表现
内存开销缓存结果常驻内存,直到依赖变化
比较开销每次渲染逐个 Object.is 比较依赖数组
认知负担开发者要手动维护依赖数组,写错就是 bug

所以判断要不要用 useMemo,本质是一道经济学的比较题:

计算成本 > 缓存成本?  → 用。

计算成本 ≤ 缓存成本?  → 别用,直接算更快。

一个 data.length > 0 是 O(1) 操作,耗时约 0.001ms。用 useMemo 包它?比较依赖数组的成本可能比计算本身还高。

不是所有值得记住的东西都值得缓存。

三、什么时候该用——三个合理场景

场景示例为什么要缓存
计算确实昂贵大数组 .filter().sort().map() 链式操作耗时 ≥ 1ms 的计算,跳过有实际收益
配合 memo 稳定子组件 propsuseMemo 返回的数组/对象传给 memo(Child)保持引用稳定,否则 memo 白包
作为其他 Hook 的依赖计算结果是 useEffect 依赖数组的一项引用不稳定会导致 Effect 反复触发

第二个场景最容易被忽略。看这个例子:

import { memo, useMemo } from 'react';

const List = memo(function List({ items }) {
  // items 引用不变 → 跳过重新渲染
  return items.map(item => <li key={item.id}>{item.text}</li>);
});

function TodoApp({ todos, tab, theme }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );

  return (
    <div className={theme}>
      <List items={visibleTodos} />
    </div>
  );
}

没有 useMemo 时,filterTodos 每次都返回一个新数组——引用变了,memo 包裹的 List 照样重新渲染。useMemo 在这里的作用不是"省计算",而是 "稳引用"

useMemo 的两个价值:省计算、稳引用。大部分人只知道前者。

四、什么时候不该用——反模式清单

❌ 包裹极轻量计算

// 这是在浪费 Hook 调用
const hasData = useMemo(() => data.length > 0, [data]);

// ✅ 直接写
const hasData = data.length > 0;

❌ 依赖引用不稳定

// options 每次渲染都是新对象 → useMemo 永远命中不了缓存
const options = { matchMode: 'whole-word', text };
const result = useMemo(() => search(items, options), [items, options]);

options 是内联对象,每次渲染都产生新引用。Object.is 一比就是 false,缓存形同虚设。

正确做法:把对象创建移到 useMemo 内部,依赖原始值:

// ✅ 直接依赖原始值 text,不依赖对象引用
const result = useMemo(() => {
  const options = { matchMode: 'whole-word', text };
  return search(items, options);
}, [items, text]);

❌ 用 useMemo 掩盖架构问题

如果一个组件因为父组件频繁 re-render 而不断执行昂贵计算,第一反应不应该是"加 useMemo",而是问:这个状态是不是放错位置了?

React 官方文档原话:

先把组件写纯、把状态放对位置、删掉不必要的 Effect,再考虑 useMemo。

这就像建筑——如果承重墙位置不对,刷再好的墙漆也救不了歪的结构。  useMemo 是装修材料,不是地基。

架构问题,不是一个 Hook 能补救的。

五、useMemo vs useCallback vs React.memo——三者关系

这三个经常被混淆,一张表理清楚:

工具缓存的是什么触发条件典型用途
useMemo计算结果(任意值)依赖数组变化缓存昂贵计算、稳定对象/数组引用
useCallback函数引用依赖数组变化稳定回调函数引用,配合 memo 使用
React.memo组件渲染结果props 变化(浅比较)跳过 props 未变的子组件重渲染

一个关键等式:

useCallback(fn, deps) === useMemo(() => fn, deps)

useCallback 就是 useMemo 的语法糖——专门用来缓存函数。如果你需要缓存一个值用 useMemo,缓存一个函数用 useCallback,防止子组件无效渲染用 React.memo

它们三个经常要配合使用:memo 是盾,useMemo/useCallback 是让盾生效的稳定引用。

六、依赖数组——缓存失效的唯一裁判

useMemo 的缓存何时失效?完全取决于依赖数组。

情况行为
所有依赖都没变(Object.is 通过)返回缓存值
任一依赖变了重新执行计算函数
忘了传依赖数组每次渲染都重新计算(白写了)
传了空数组 []永不失效,只计算一次(类似常量)

最常见的 bug 是忘传依赖数组

// ❌ 没有第二个参数 → 每次渲染都执行,等于没写 useMemo
const result = useMemo(() => expensiveCalculation(data));

// ✅ 记得传依赖
const result = useMemo(() => expensiveCalculation(data), [data]);

还有一个容易踩坑的:依赖数组里放了对象/函数。JavaScript 中 {} !== {},即使内容完全一样,引用不同就算"变了"。所以依赖数组尽量放原始值(string、number、boolean),不要放每次渲染都重新创建的对象。

七、缓存边界意识——跨越 useMemo 的思维模型

用户提出了一个很精准的观察:useMemo 训练的其实是缓存边界意识

任何缓存策略的核心,都是同一道判断题:重新计算更便宜,还是存起来复用更便宜?

这和设计模式的思路完全一致:

模式权衡核心问题
Builder重新组装 vs 克隆已有对象构建成本高不高?
Prototype深拷贝 vs 从头 new创建成本高不高?
useMemo重新计算 vs 复用缓存计算成本高不高?

它们的底层逻辑是同一个:在"重建"和"复用"之间画一条线,线画在哪里,取决于你对成本的判断。

八、实战判断清单

最后给一份速查流程。下次纠结要不要加 useMemo 时,过一遍这个:

步骤问题答案 → 行动
1组件结构合理吗?状态放对位置了吗?没有 → 先改结构
2有不必要的 Effect 在更新 state 吗?有 → 先删 Effect
3这个计算耗时 ≥ 1ms 吗?(用 console.time 量)没到 → 不用 memo
4这个值是传给 memo 子组件的 prop 吗?是 → 用 useMemo 稳引用
5这个值是其他 Hook 的依赖吗?是 → 用 useMemo 稳引用
6依赖数组里有对象/函数吗?有 → 改成依赖原始值

如果你只想带走一句话,我建议记这个:

先改结构,再谈缓存——useMemo 是性能优化的最后一步,不是第一步。

参考原文:

• React 官方文档 — useMemo

qrcode_for_gh_6a9e7f3719d6_344.jpg