一、为什么需要性能优化?
React 的核心机制是:状态变化 → 组件函数重新执行 → 生成新的 UI。
但问题在于:
-
每次重渲染,所有变量、函数都会重新创建
-
即使某些数据或逻辑并未改变,也会被重复执行
-
若涉及昂贵计算或传递给子组件的函数/对象,会导致:
- CPU 浪费(如循环百万次)
- 子组件无谓重渲染(因 props 引用变化)
🎯 目标:只在真正需要时才重新计算或渲染。
二、useMemo:缓存计算结果
✅ 核心作用
缓存函数的返回值,避免重复执行昂贵计算。
示例解析
jsx
预览
// 昂贵计算:循环 n * 100 万次
function slowSum(n) {
let sum = 0;
for (let i = 0; i < n * 1000000; i++) sum += i;
return sum;
}
export default function App() {
const [count, setCount] = useState(0);
const [keyword, setKeyword] = useState('');
const [num, setNum] = useState(0);
const list = ['apple', 'banana', 'orange', 'pear'];
// ✅ 仅当 keyword 变化时才重新过滤
const filterList = useMemo(() => {
return list.filter(item => item.includes(keyword));
}, [keyword]);
// ✅ 仅当 num 变化时才重新计算 slowSum
const result = useMemo(() => slowSum(num), [num]);
return (
<div>
<p>结果:{result}</p>
<button onClick={() => setNum(num + 1)}>num + 1</button>
<input value={keyword} onChange={e => setKeyword(e.target.value)} />
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
{filterList.map(item => <li key={item}>{item}</li>)}
</div>
);
}
关键点说明:
表格
| 问题 | 未优化表现 | 使用 useMemo 后 |
|---|---|---|
| 点击 “count + 1” | filterList 和 slowSum 都会重新执行 | 完全跳过,因依赖项未变 |
| 输入关键词 | 仅 filterList 重新计算 | ✅ 符合预期 |
| 修改 num | 仅 slowSum 重新计算 | ✅ 符合预期 |
💡 注意:
"".includes("")返回true,所以空输入会显示全部列表——这是 JS 行为,非 bug。
三、useCallback:缓存函数引用
✅ 核心作用
缓存函数本身,避免每次渲染都创建新函数,从而防止子组件因 props 引用变化而重渲染。
场景:配合 React.memo 优化子组件
jsx
预览
import { useState, memo, useCallback } from 'react';
// ✅ 用 memo 包裹子组件:仅当 props 引用变化时才重渲染
const Child = memo(({ count, handleClick }) => {
console.log('Child 重新渲染');
return <div onClick={handleClick}>子组件 {count}</div>;
});
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// ❌ 普通写法:每次父组件重渲染,handleClick 都是新函数
// const handleClick = () => console.log('click');
// ✅ 用 useCallback 缓存函数
const handleClick = useCallback(() => {
console.log('click');
}, []); // 依赖为空,函数永不更新
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<p>num: {num}</p>
<button onClick={() => setNum(num + 1)}>num + 1</button>
{/* 传递缓存后的函数 */}
<Child count={count} handleClick={handleClick} />
</div>
);
}
行为对比:
表格
| 操作 | 未用 useCallback | 使用 useCallback |
|---|---|---|
| 点击 “num + 1” | Child 重渲染(因 handleClick 是新函数) | Child 不重渲染(handleClick 引用不变) |
| 点击 “count + 1” | Child 重渲染(因 count 变) | Child 重渲染(合理,因 count 确实变了) |
📌 关键思想:React 数据流 = 父管数据,子管展示。通过
memo + useCallback实现“按需更新”。
四、useMemo vs useCallback:一张表说清区别
表格
| 特性 | useMemo | useCallback |
|---|---|---|
| 用途 | 缓存值(计算结果) | 缓存函数 |
| 等价写法 | useMemo(() => compute(), deps) | useMemo(() => fn, deps) |
| 典型场景 | 过滤列表、格式化数据、复杂数学运算 | 传递给子组件的事件回调 |
| 返回类型 | 任意值(数字、数组、对象等) | 函数 |
✅ 记住:
useCallback(fn, deps)≈useMemo(() => fn, deps)
五、注意事项 & 常见误区
1. 不要为了优化而优化
- 对简单操作(如
a + b)使用useMemo反而增加内存开销。 - 先测量性能瓶颈(用 React DevTools Profiler),再决定是否优化。
2. 依赖项必须完整
js
编辑
// ❌ 危险:list 变了也不会更新 filterList
const filterList = useMemo(() => list.filter(...), []);
// ✅ 正确
const filterList = useMemo(() => list.filter(...), [list, keyword]);
3. 函数依赖状态时,需谨慎
js
编辑
const handleClick = useCallback(() => {
console.log(count); // 依赖 count
}, [count]); // 必须加入依赖!
否则会捕获旧的闭包值(stale closure)。
4. useMemo 不能替代 useEffect
useMemo用于同步计算,不应用于副作用(如发请求、订阅)。- 副作用请用
useEffect。
六、总结要点(方便复习)
✅ useMemo 适用场景:
- 过滤/映射大型数组
- 昂贵计算(如
slowSum) - 派生状态(如格式化日期、计算总额)
✅ useCallback 适用场景:
- 将回调函数传递给
React.memo包裹的子组件 - 避免因函数引用变化导致子组件重渲染
✅ 黄金法则:
“只缓存真正昂贵或影响渲染的值/函数”
✅ 组合拳:
js
编辑
父组件:useCallback 缓存函数 + useMemo 缓存数据
子组件:React.memo 阻止无谓渲染
七、拓展思考
Q:Vue 的 computed 和 React 的 useMemo 有何异同?
-
相同:都用于派生状态的缓存。
-
不同:
- Vue 自动追踪依赖,无需手动声明;
- React 显式声明依赖,更灵活但需开发者负责正确性。
Q:能否用 useRef 替代 useMemo?
useRef可存储值且不触发重渲染,但不会自动响应依赖变化。useMemo更语义化、自动响应依赖,优先使用 useMemo。