5分钟精通 useMemo

useMemo 的设计初衷是 "用最小开销完成计算并用于渲染"

1. 引言:为什么我们需要关注 useMemo

优化数据看板时,我踩了个典型的性能坑 ------ 用户在搜索框输入关键词时,页面每输入一个字符就卡顿 1 秒。打开 React DevTools 一看:10000 条商品数据的过滤 + 排序逻辑,竟然在每次输入时都重新执行,计算耗时高达120ms !这就是 React 函数组件的 "隐形陷阱":无论状态是否相关,组件重渲染时所有代码都会重新跑一遍

后来用useMemo缓存计算结果,同样的操作耗时骤降至8ms ,输入框瞬间丝滑。这个案例揭示了一个核心问题:随着项目复杂度提升,未经优化的重复计算会成为性能瓶颈。本文会通过3 个实战案例带你吃透 useMemo,包括:

  • 列表渲染优化:从输入卡顿到秒响应

  • 复杂计算隔离:仪表盘数据处理提速 300%

  • React 2025 新变化:编译器自动优化与手动优化的边界

🌟 为什么这篇文章值得读?

不同于纯理论文章,我会结合真实项目中的性能瓶颈(附代码 + 耗时对比),告诉你什么时候用 useMemo 能救命,什么时候用了反而拖慢性能

2. useMemo 基础:语法到本质

2.1 核心语法与参数

useMemo的语法非常简单,但 90% 的人容易在依赖数组上栽跟头:

javascript 复制代码
const memoizedValue = useMemo(calculateValue, dependencies);
  • 第一个参数:需要缓存的计算函数(必须是纯函数,没有副作用)
  • 第二个参数:依赖数组,只有数组中的变量变化时,才会重新计算
  • 返回值:缓存的计算结果(首次渲染时执行函数,后续复用缓存)

2.2 与 useCallback 的区别

很多人搞不清这两个 Hook 的区别,其实一句话就能讲透:

场景 useMemo useCallback
缓存什么 计算结果(值) 函数引用
典型用法 过滤排序大列表、复杂数据转换 传递给子组件的事件处理函数
本质关系 useCallback(fn, deps)useMemo(() => fn, deps) ------

实战代码对比(过滤商品列表):

javascript

javascript 复制代码
// 未优化:每次渲染重新过滤5000条数据
function ProductList({ products, category }) {
  // 问题:即使category不变,过滤逻辑也会重复执行
  const filtered = products.filter(p => p.category === category);
  return <List items={filtered} />;
}

// 优化后:仅category变化时才重新过滤
const filtered = useMemo(() => {
  console.time('filter');
  const result = products.filter(p => p.category === category);
  console.timeEnd('filter'); // 打印耗时:首次80ms,缓存后0.1ms
  return result;
}, [products, category]); // 依赖数组必须精确

3. useMemo 效率:定量分析,什么时候效率比较好

3.1 性能收益的量化标准

不是所有计算都需要用 useMemo!我在项目中总结出一个简单判断法则:

javascript

javascript 复制代码
// 3行代码判断是否需要useMemo
console.time('compute');
const result = heavyCalculation(); // 执行你的计算逻辑
console.timeEnd('compute'); // 如果耗时>50ms,再考虑用useMemo

实测数据(基于 10000 条数据的排序计算):

计算复杂度 未使用 useMemo 使用 useMemo(首次) 使用 useMemo(缓存)
简单过滤(1 层循环) 30ms 30ms 0.1ms
复杂排序(多层比较) 120ms 120ms 0.1ms
数据聚合(嵌套循环) 280ms 280ms 0.1ms

结论:计算耗时超过 50ms,或组件每秒重渲染 > 10 次时,useMemo 的收益才明显。

3.2 真实场景性能对比

案例:电商筛选功能优化

某生鲜平台的商品列表页,用户输入关键词时需要实时过滤 5000 条商品数据。未优化前,输入框打字有明显延迟:

javascript

javascript 复制代码
// 未优化:每次输入触发完整计算(80ms/次)
function SearchBar({ products }) {
  const [keyword, setKeyword] = useState('');
  
  // 问题:即使关键词相同(如快速按退格键),也会重新计算
  const results = products
    .filter(p => p.name.includes(keyword))
    .sort((a, b) => b.sales - a.sales);

  return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />;
}

用 useMemo 优化后,输入响应速度提升 10 倍:

javascript 复制代码
// 优化后:仅关键词变化时重新计算
const results = useMemo(() => {
  return products
    .filter(p => p.name.includes(keyword))
    .sort((a, b) => b.sales - a.sales);
}, [products, keyword]); // 精准依赖

4. useMemo 源码分析 - 为何它能提高效率

//源码地址 packages/react-reconciler/src/ReactFiberHooks.js

4.1 底层数据结构与流程

React 内部通过 "Hook 链表" 存储 useMemo 的状态,每个节点长这样:

javascript 复制代码
{
  memoizedState: [缓存值, 依赖数组], // 核心:存储计算结果和依赖
  next: null // 指向下一个Hook
}

工作流程分两步

  1. 首次渲染(mountMemo)

    执行计算函数,把结果和依赖存入 memoizedState

javascript 复制代码
function mountMemo(nextCreate, deps) {
  const hook = mountWorkInProgressHook(); // 为当前 Fiber 新建 hook 记录
  const nextDeps = deps === undefined ? null : deps; // 未传 deps → 置为 null(表示每次重算)
  const nextValue = nextCreate(); // 计算初始值

  if (shouldDoubleInvokeUserFnsInHooksDEV) { // 仅 DEV 严格模式:二次调用以暴露副作用
    setIsStrictModeForDevtools(true);
    try {
      nextCreate();
    } finally {
      setIsStrictModeForDevtools(false);
    }
  }

  hook.memoizedState = [nextValue, nextDeps]; // 缓存 [值, 依赖]
  return nextValue; // 返回给组件
}
  1. 更新阶段(updateMemo)

    对比新旧依赖,相同则复用缓存,不同则重新计算

javascript 复制代码
function updateMemo(nextCreate, deps) {
 const hook = updateWorkInProgressHook(); // 取到对应的 hook 记录
  const nextDeps = deps === undefined ? null : deps; // 未传 deps → null(表示不走依赖比较)
  const prevState = hook.memoizedState; // 之前的 [value, deps]

  if (nextDeps !== null) { // 只有提供了依赖数组才尝试复用
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) { // 浅比较相等 → 直接复用旧值
      return prevState[0];
    }
  }

  const nextValue = nextCreate(); // 依赖变了或没提供依赖 → 重算
  if (shouldDoubleInvokeUserFnsInHooksDEV) { // 仅 DEV 严格模式:再调用一次
    setIsStrictModeForDevtools(true);
    try {
      nextCreate();
    } finally {
      setIsStrictModeForDevtools(false);
    }
  }

  hook.memoizedState = [nextValue, nextDeps]; // 覆盖缓存
  return nextValue;
}

4.2 依赖比较的核心逻辑

React 用Object.is进行浅比较,这意味着:

  • 基本类型(数字 / 字符串):值相同则视为相等

  • 引用类型(对象 / 数组):引用相同才视为相等(即使内容一样)

javascript 复制代码
// 这些情况都会导致依赖变化!
const [list, setList] = useState([]);
setList([...list, newItem]); // 创建新数组,引用变化
useMemo(() => {}, [list]); // 依赖变化,缓存失效

5. 避坑指南:常见使用错误与解决方案

5.1 误区一:给简单计算加缓存

反例:给 a+b 这种简单计算用 useMemo,纯属浪费性能

javascript 复制代码
// ❌ 错误:简单计算不需要缓存
const total = useMemo(() => a + b, [a, b]);

5.2 误区二:依赖数组不全

反例:函数里用到的变量没写进依赖数组,导致闭包陷阱

javascript 复制代码
// ❌ 错误:count变化时,increment不会更新
const [count, setCount] = useState(0);
const increment = useMemo(() => {
  return () => setCount(count + 1); // count未在依赖中
}, []); // 正确依赖应为 [count]

5.3 误区三:React 2025 年还需要手动优化吗?

React 19 引入了编译器自动优化,但这两种情况仍需手动处理:

  1. 第三方库强制要求稳定引用
    如 ECharts 图表组件,数据引用变化会导致重绘闪烁:
javascript 复制代码
// ✅ 正确:用useMemo稳定数据引用
const chartData = useMemo(() => ({
  xAxis: [...],
  series: [...]
}), [rawData]); // 仅原始数据变化时更新
  1. 极端性能敏感场景
    如动画帧计算、实时数据可视化(每秒更新 60 次)

6. 结语:理性看待性能优化

总结出性能优化的 "三不原则":

  1. 不盲目加 useMemo:先用量化数据证明有性能问题

  2. 不忽视基础优化:组件拆分、虚拟列表可能比 useMemo 更有效

  3. 不依赖单一方案:结合 React.memo、useCallback 形成优化组合拳

📌 最后送大家一个检查清单

当你想加 useMemo 时,先问自己:

① 这个计算耗时超过 50ms 吗?

② 依赖数组能写全吗?

③ 有没有更简单的优化方式(如状态拆分)?

React 性能优化的本质,是让代码只做必要的工作。希望这篇文章能帮你避开 useMemo 的坑,写出既丝滑又易维护的 React 应用!

相关推荐
Fly-ping10 分钟前
【前端八股文面试题】【JavaScript篇3】DOM常⻅的操作有哪些?
前端
2301_8109703914 分钟前
Wed前端第二次作业
前端·html
不浪brown19 分钟前
全部开源!100+套大屏可视化模版速来领取!(含源码)
前端·数据可视化
iOS大前端海猫21 分钟前
drawRect方法的理解
前端
姑苏洛言36 分钟前
有趣的 npm 库 · json-server
前端
知否技术40 分钟前
Vue3项目中轻松开发自适应的可视化大屏!附源码!
前端·数据可视化
Hilaku43 分钟前
为什么我坚持用git命令行,而不是GUI工具?
前端·javascript·git
用户adminuser44 分钟前
深入理解 JavaScript 中的闭包及其实际应用
前端
heartmoonq1 小时前
个人对于sign的理解
前端
ZzMemory1 小时前
告别移动端适配烦恼!pxToViewport 凭什么取代 lib-flexible?
前端·css·面试