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

相关推荐
bemyrunningdog1 小时前
创建 React 项目指南:Vite 与 Create React App 详
前端·react.js·前端框架
大雷神1 小时前
DevUI 实战教程:从零构建电商后台管理系统(完整版)
前端·javascript·华为·angular.js
come112341 小时前
现代前端技术栈关系详解 (PHP 开发者特供版)
开发语言·前端·php
合作小小程序员小小店1 小时前
web网页开发,在线%图书管理%系统,基于Idea,html,css,jQuery,java,ssm,mysql。
java·前端·后端·mysql·jdk·intellij-idea
猪八戒1.02 小时前
onenet接口
开发语言·前端·javascript·嵌入式硬件
程序猿小蒜2 小时前
基于Spring Boot的宠物领养系统的设计与实现
java·前端·spring boot·后端·spring·宠物
合作小小程序员小小店2 小时前
web网页开发,在线%食堂管理%系统,基于Idea,html,css,jQuery,java,ssm,mysql。
java·前端·mysql·html·intellij-idea·jquery
人工智能训练2 小时前
Windows中如何将Docker安装在E盘并将Docker的镜像和容器存储在E盘的安装目录下
linux·运维·前端·人工智能·windows·docker·容器
90后小陈老师2 小时前
用户管理系统 07 项目前端初始化 | 新手实战 | 期末实训 | Java+SpringBoot+Vue
java·前端·spring boot