React 性能优化教程:useMemo 和 useCallback 的正确使用方式

1. 为什么 React 中需要缓存?

原因只有一个:

函数组件每次渲染都会重新执行:所有对象、数组、函数、计算全部重新生成。

示例:

typescript 复制代码
const arr = [1, 2, 3]; // 每次渲染都是新引用
const fn = () => {};   // 新函数引用
const v = heavyCompute(data); // 昂贵计算重新执行

这些导致:

  • 子组件不必要渲染
  • useEffect 频繁触发
  • 重计算导致卡顿

React 提供了 引用级缓存机制useMemouseCallback


2. useMemo 与 useCallback 的本质区别

✔ useMemo ------ 缓存"值"

typescript 复制代码
const memoValue = useMemo(() => compute(x), [x]);

缓存的不是函数,而是计算结果。


✔ useCallback ------ 缓存"函数引用"

typescript 复制代码
const memoFn = useCallback(() => doSomething(x), [x]);

本质等价于:

typescript 复制代码
useMemo(() => () => doSomething(x), [x]);

useCallback 就是 useMemo 的语法糖。


3. 它们真正解决的问题是什么?

🧩 问题一:引用不稳定导致子组件重复渲染

typescript 复制代码
<Child config={{ a: 1 }} />

config 每次都是新对象 → 子组件必渲染。


🧩 问题二:useEffect 的依赖不断变化 → 无限循环

typescript 复制代码
useEffect(() => {
  api.fetch(filters);
}, [filters]);

filters 是 {} 字面量 → 无限触发。


🧩 问题三:昂贵计算反复执行 → 性能卡顿

比如:

  • 列表排序、过滤
  • 矩阵运算
  • 大规模数据 transform
  • Markdown/图表解析

4. useMemo vs useCallback 场景划分(最重要的表)

目的 应使用
缓存昂贵计算结果 useMemo
缓存对象/数组引用 useMemo
稳定 useEffect 的依赖数组 useMemo
子组件需要稳定的回调函数 useCallback
避免回调因重新创建导致子组件渲染 useCallback
避免事件监听 remove 失败 useCallback
缓存函数闭包环境 useCallback

5. useMemo 的底层运行机制(深度解析)

5.1 React 的依赖比较算法

typescript 复制代码
function areDepsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) return false;

  for (let i = 0; i < prevDeps.length; i++) {
    if (!Object.is(nextDeps[i], prevDeps[i])) {
      return false;
    }
  }

  return true;
}

关键点:

  • 使用 Object.is(比 === 更严格)
  • 依赖项必须是稳定引用
  • 依赖项数组的顺序不能变化

5.2 useMemo 内部生命周期(更新阶段)

伪代码:

typescript 复制代码
if (areDepsEqual(nextDeps, prevDeps)) {
  return prevValue;       // 返回缓存
} else {
  const nextValue = create(); 
  saveToHook(nextValue, nextDeps);
  return nextValue;
}

缓存命中 → 不执行计算
缓存 miss → 执行工厂函数并更新缓存


6. useMemo 的性能特性与基准分析

判断是否需要 useMemo,一个非常实用的标准:

复制代码
计算耗时 > 1ms → 适合使用 useMemo

大量测试表明:

  • useMemo 的缓存开销约为 0.03ms ~ 0.1ms
  • 当你的函数执行时间 < 0.1ms 时,useMemo 反而会更慢

6.1 性能测量示例

typescript 复制代码
const processedData = useMemo(() => {
  const start = performance.now();

  const result = heavyCompute(data);

  const end = performance.now();
  console.log(`Compute took ${end - start}ms`);

  return result;
}, [data]);

7. 高级模式:工程级 useMemo 组合策略

7.1 分层缓存(Layered Memoization)

typescript 复制代码
// 1. 过滤
const filtered = useMemo(() => {
  return users.filter(u => filters.every(f => f(u)));
}, [users, filters]);

// 2. 排序
const sorted = useMemo(() => {
  return [...filtered].sort(sortFn);
}, [filtered, sortFn]);

// 3. 衍生数据
const stats = useMemo(() => {
  return { total: sorted.length, avg: avg(sorted) };
}, [sorted]);

这样可以大幅减少重复计算。


7.2 智能缓存(Smart Memo)

typescript 复制代码
const useSmartMemo = (factory, deps, maxAge = 1000) => {
  const cacheRef = useRef(null);

  return useMemo(() => {
    const now = Date.now();

    if (cacheRef.current &&
        now - cacheRef.current.timestamp < maxAge &&
        areDepsEqual(cacheRef.current.deps, deps)) {
      return cacheRef.current.value;
    }

    const value = factory();
    cacheRef.current = { value, deps, timestamp: now };
    return value;
  }, deps);
};

8. useMemo 依赖管理:常见陷阱与最佳实践

❌ 8.1 依赖项是对象字面量 → 永远变化

示例:

typescript 复制代码
useMemo(() => compute(v), [{ x: 1 }]);

每次渲染 { x: 1 } 都是新对象 → useMemo 永远失效。

🟢 正确写法:

typescript 复制代码
const config = useMemo(() => ({ x: 1 }), []);
useMemo(() => compute(v, config), [v, config]);

❌ 8.2 闭包陷阱

typescript 复制代码
const fn = useMemo(() => () => setCount(count + 1), [count]);

问题:count 是旧值。

🟢 正确:

typescript 复制代码
const fn = useCallback(() => setCount(c => c + 1), []);

9. useCallback 深度理解与闭包陷阱

闭包会保存旧的变量环境,所以 useCallback 通常用于:

  • 保持函数引用稳定
  • 保持闭包行为正确(例如函数式更新)
  • 配合 React.memo 避免子组件重复渲染

10. 并发模式下 useMemo 的行为

React 在 concurrent mode 下:

  • useMemo 可能被打断、多次执行
  • 可能在渲染中被"丢弃"

因此 useMemo 不能用于副作用,只能用于纯计算。

高级用法:useDeferredValue + useMemo

typescript 复制代码
const deferred = useDeferredValue(query);

const results = useMemo(() => search(deferred), [deferred]);

11. 什么时候不该用 useMemo?

❌ 计算量极小

❌ 依赖项频繁变化(缓存命中率低)

❌ 组件每次都一定会重新渲染

❌ 只是为了"看起来优雅"

🟢 应使用 useMemo 的情况:

  • 大规模数组处理
  • 大量 derived data
  • 传递对象给 React.memo 子组件
  • 使用 useEffect 依赖且需要稳定引用

12. useMemo/useCallback 使用决策树(工程标准版)

是 否 是 否 是 否 是 否 该计算昂贵吗? 使用 useMemo 该值是否作为 props 传递? 子组件是否 memo 化 该函数是否作为依赖传给 useEffect? 使用 useMemo / useCallback 不需要 memo 使用 useCallback 不需要


13. 总结:一句话记住 useMemo 和 useCallback

1. useMemo 缓存值,为了解决昂贵计算和引用稳定性问题。
2. useCallback 缓存函数引用,为了解决函数传递导致的重复渲染问题。
3. 不要为了"看起来高级"滥用它们------衡量成本和收益才是工程思维。

相关推荐
Mr.Jessy14 小时前
JavaScript高级:构造函数与原型
开发语言·前端·javascript·学习·ecmascript
白兰地空瓶16 小时前
🚀你以为你在写 React?其实你在“搭一套前端操作系统”
前端·react.js
爱上妖精的尾巴17 小时前
6-4 WPS JS宏 不重复随机取值应用
开发语言·前端·javascript
似水流年QC17 小时前
深入探索 WebHID:Web 标准下的硬件交互实现
前端·交互·webhid
陪我去看海17 小时前
测试 mcp
前端
speedoooo18 小时前
在现有App里嵌入一个AI协作者
前端·ui·小程序·前端框架·web app
全栈胖叔叔-瓜州18 小时前
关于llamasharp 大模型多轮对话,模型对话无法终止,或者输出角色标识User:,或者System等角色标识问题。
前端·人工智能
三七吃山漆18 小时前
攻防世界——wife_wife
前端·javascript·web安全·网络安全·ctf
用户479492835691518 小时前
面试官问"try-catch影响性能吗",我用数据打脸
前端·javascript·面试
GISer_Jing19 小时前
前端营销技术实战:数据+AI实战指南
前端·javascript·人工智能