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

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

原因只有一个:

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

示例:

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

这些导致:

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

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


2. useMemo 与 useCallback 的本质区别

✔ useMemo —— 缓存“值”

const memoValue = useMemo(() => compute(x), [x]);

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


✔ useCallback —— 缓存“函数引用”

const memoFn = useCallback(() => doSomething(x), [x]);

本质等价于:

useMemo(() => () => doSomething(x), [x]);

useCallback 就是 useMemo 的语法糖。


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

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

<Child config={{ a: 1 }} />

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


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

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 的依赖比较算法

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 内部生命周期(更新阶段)

伪代码:

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 性能测量示例

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)

// 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)

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 依赖项是对象字面量 → 永远变化

示例:

useMemo(() => compute(v), [{ x: 1 }]);

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

🟢 正确写法:

const config = useMemo(() => ({ x: 1 }), []);
useMemo(() => compute(v, config), [v, config]);

❌ 8.2 闭包陷阱

const fn = useMemo(() => () => setCount(count + 1), [count]);

问题:count 是旧值。

🟢 正确:

const fn = useCallback(() => setCount(c => c + 1), []);

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

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

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

10. 并发模式下 useMemo 的行为

React 在 concurrent mode 下:

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

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

高级用法:useDeferredValue + useMemo

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. 不要为了“看起来高级”滥用它们——衡量成本和收益才是工程思维。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值