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
}
工作流程分两步:
-
首次渲染(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; // 返回给组件
}
-
更新阶段(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 引入了编译器自动优化,但这两种情况仍需手动处理:
- 第三方库强制要求稳定引用
如 ECharts 图表组件,数据引用变化会导致重绘闪烁:
javascript
// ✅ 正确:用useMemo稳定数据引用
const chartData = useMemo(() => ({
xAxis: [...],
series: [...]
}), [rawData]); // 仅原始数据变化时更新
- 极端性能敏感场景
如动画帧计算、实时数据可视化(每秒更新 60 次)
6. 结语:理性看待性能优化
总结出性能优化的 "三不原则":
-
不盲目加 useMemo:先用量化数据证明有性能问题
-
不忽视基础优化:组件拆分、虚拟列表可能比 useMemo 更有效
-
不依赖单一方案:结合 React.memo、useCallback 形成优化组合拳
📌 最后送大家一个检查清单
当你想加 useMemo 时,先问自己:
① 这个计算耗时超过 50ms 吗?
② 依赖数组能写全吗?
③ 有没有更简单的优化方式(如状态拆分)?
React 性能优化的本质,是让代码只做必要的工作。希望这篇文章能帮你避开 useMemo 的坑,写出既丝滑又易维护的 React 应用!