引言
在 React 应用开发中,性能优化是每个开发者都必须面对的课题。很多团队在遇到渲染性能问题时,第一反应就是给组件加上 React.memo,给函数加上 useMemo。然而,滥用这些优化手段反而可能导致性能下降。
今天我们来深入探讨 memo 和 useMemo 的工作原理,以及如何在实际项目中正确使用它们。
React 渲染机制回顾
在深入优化之前,我们需要理解 React 的渲染机制:
- 状态变化触发渲染:当组件的 state 或 props 发生变化时,React 会重新渲染该组件及其子组件
- 虚拟 DOM 比对:React 会生成新的虚拟 DOM 树,并与旧树进行比对(diff)
- 最小化真实 DOM 操作:只有实际变化的部分才会更新到真实 DOM
关键点:即使父组件重新渲染,子组件的 props 如果没有实际变化,理论上子组件不需要重新渲染。但默认情况下,React 会重新渲染所有子组件。
React.memo:组件级别的记忆化
React.memo 是一个高阶组件,用于记忆化函数组件的渲染结果。
基本用法
javascript
import React from 'react';
// 未经优化的组件 - 每次父组件渲染都会重新渲染
const ChildComponent = ({ name, count }) => {
console.log('ChildComponent rendered');
return <div>{name}: {count}</div>;
};
// 使用 React.memo 优化
const MemoizedChild = React.memo(({ name, count }) => {
console.log('MemoizedChild rendered');
return <div>{name}: {count}</div>;
});
// 自定义比较函数
const CustomMemoChild = React.memo(
({ name, count, data }) => {
console.log('CustomMemoChild rendered');
return <div>{name}: {count}</div>;
},
(prevProps, nextProps) => {
// 只比较 name 和 count,忽略 data
return prevProps.name === nextProps.name &&
prevProps.count === nextProps.count;
}
);
常见陷阱
陷阱 1:对象/数组/函数作为 props
ini
// ❌ 错误示范 - 每次渲染都会创建新对象,memo 失效
function Parent() {
const [count, setCount] = useState(0);
return (
<MemoizedChild
config={{ theme: 'dark' }} // 每次都是新对象引用
onClick={() => {}} // 每次都是新函数引用
/>
);
}
// ✅ 正确示范 - 使用 useMemo/useCallback 稳定引用
function Parent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ theme: 'dark' }), []);
const handleClick = useCallback(() => {}, []);
return (
<MemoizedChild
config={config}
onClick={handleClick}
/>
);
}
陷阱 2:过度使用 memo
javascript
// ❌ 不推荐 - 简单组件不需要 memo
const SimpleLabel = React.memo(({ text }) => <span>{text}</span>);
// ✅ 推荐 - 仅在以下情况使用 memo:
// 1. 组件渲染开销大
// 2. 组件接收复杂对象/数组 props
// 3. 父组件频繁渲染但子组件 props 稳定
useMemo:值级别的记忆化
useMemo 用于记忆化计算结果,避免每次渲染都重新计算。
基本用法
javascript
import React, { useMemo, useState } from 'react';
function ExpensiveComponent({ items, filter }) {
// 昂贵的计算操作
const filteredItems = useMemo(() => {
console.log('Expensive filtering...');
return items
.filter(item => item.category === filter)
.sort((a, b) => a.priority - b.priority)
.map(item => ({
...item,
computed: heavyComputation(item)
}));
}, [items, filter]); // 仅当 items 或 filter 变化时重新计算
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
实际应用场景
场景 1:复杂计算
javascript
function Dashboard({ data }) {
// 数据统计计算
const statistics = useMemo(() => {
return {
total: data.reduce((sum, item) => sum + item.value, 0),
average: data.reduce((sum, item) => sum + item.value, 0) / data.length,
max: Math.max(...data.map(item => item.value)),
min: Math.min(...data.map(item => item.value)),
};
}, [data]);
return <StatsPanel stats={statistics} />;
}
场景 2:依赖稳定的对象引用
javascript
function Form({ onSubmit }) {
// 表单配置对象 - 需要稳定引用
const formConfig = useMemo(() => ({
validateOnBlur: true,
validateOnChange: false,
shouldUnregister: false,
}), []);
return <FormRenderer config={formConfig} />;
}
常见误区
误区 1:useMemo 总是能提升性能
scss
// ❌ 错误 - 简单计算不需要 useMemo
const doubled = useMemo(() => count * 2, [count]);
// ✅ 正确 - 仅在计算开销大于记忆化开销时使用
// useMemo 本身也有成本:依赖比较 + 缓存管理
误区 2:依赖数组不完整
scss
// ❌ 危险 - 可能使用过时的值
function Component({ userId }) {
const user = useMemo(() => {
return fetchUser(userId); // 如果 userId 变化,这里不会重新执行
}, []); // 缺少 userId 依赖
// ✅ 正确
const user = useMemo(() => {
return fetchUser(userId);
}, [userId]);
}
性能优化检查清单
在使用 memo 和 useMemo 之前,请确认:
- 组件渲染确实存在性能问题(使用 React DevTools Profiler 验证)
- 组件接收的 props 包含对象/数组/函数
- 父组件渲染频率高于子组件需要的渲染频率
- 计算操作确实是"昂贵"的(遍历大数组、复杂数学运算等)
- 依赖数组完整且正确
总结
React.memo 和 useMemo 是强大的性能优化工具,但它们不是银弹:
- 先测量,再优化:使用 React DevTools Profiler 找到真正的性能瓶颈
- 理解原理:明白它们何时生效、何时失效
- 避免滥用:简单的组件和计算不需要优化
- 注意依赖:确保依赖数组完整,避免闭包陷阱
下一篇我们将继续探讨 useCallback 和 useTransition 的使用技巧,进一步完善 React 性能优化知识体系。
📚 延伸阅读: