React 性能优化:memo 与 PureComponent
为什么性能优化是 React 开发者的必修课
React 的声明式模型使得 UI 开发变得简单:你描述 UI 应该是什么样子,React 负责更新实际的 DOM。但这种便利性是有代价的——如果不小心,很容易写出不必要的渲染,导致应用变慢。
理解 React 的渲染机制和性能优化工具,是构建高性能 React 应用的必要条件。这不仅仅是"让应用变快"的问题,更是理解 React 工作原理的体现。
面试定位:性能优化是 React 面试中高阶话题。面试官通过候选人对 React.memo 和 PureComponent 的理解深度、是否能识别"不值得 memo 的场景"、是否能解释 memo 与 useMemo 的区别,来评估其对 React 渲染模型的理解和性能优化的工程判断能力。
一、React.memo 的实现原理:浅比较 props
1.1 什么是 React.memo
React.memo 是一个高阶组件(HOC),它包装一个组件,使其仅在 props 变化时重新渲染。
const MemoizedComponent = React.memo(function MyComponent(props) {
return <div>{props.name}</div>;
});
React.memo 的第二个参数是一个自定义比较函数:
const MemoizedComponent = React.memo(
function MyComponent(props) {
return <div>{props.name}</div>;
},
(prevProps, nextProps) => {
// 返回 true 表示 props 相等,不重新渲染
// 返回 false 表示 props 不相等,需要重新渲染
return prevProps.name === nextProps.name;
}
);
1.2 浅比较(Shallow Comparison)机制
React.memo 默认使用浅比较来判断 props 是否变化:
// React.memo 内部的比较逻辑(伪代码)
function shallowCompare(prevProps, nextProps) {
const keys = Object.keys({ ...prevProps, ...nextProps });
for (const key of keys) {
if (prevProps[key] !== nextProps[key]) {
return false; // 有变化,需要重新渲染
}
}
return true; // 没有变化,跳过渲染
}
浅比较的问题:对于引用类型的 props(对象、数组、函数),浅比较只比较引用,不比较内容。
function Parent() {
const [count, setCount] = useState(0);
return (
// 每次 count 变化,renderItem 都会被重新渲染
// 因为 style 对象每次都是新引用
<MemoizedList
items={['a', 'b', 'c']}
style={{ margin: 16 }} // 每次渲染都是新对象 {}
/>
);
}
1.3 React.memo 的缓存策略
React.memo 缓存的是组件的渲染结果,而不是组件函数本身。当 props 没有变化时,React 直接复用上次渲染的结果,不需要再次执行组件函数。
const MemoizedComponent = React.memo(function Component({ name, age }) {
// 只有 name 或 age 变化时,这段代码才会重新执行
console.log('Component rendered');
return <div>{name}: {age}</div>;
});
二、什么时候应该用 React.memo
2.1 值得使用 memo 的场景
场景一:组件渲染频繁,但 props 变化少
// 一个大型列表中的每个列表项
function ListItem({ item, onSelect }) {
// 只有 item 变化或 onSelect 变化时才需要重新渲染
return (
<div onClick={() => onSelect(item.id)}>
<span>{item.name}</span>
</div>
);
}
const MemoizedListItem = React.memo(ListItem);
场景二:组件树深层嵌套,父组件频繁更新
function App() {
const [theme, setTheme] = useState('light');
return (
<div className={theme}>
<Header /> {/* Header 不需要重新渲染 */}
<Sidebar /> {/* Sidebar 不需要重新渲染 */}
<Content onThemeChange={setTheme} /> {/* 只有 Content 需要 */}
</div>
);
}
场景三:组件有重计算逻辑
const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
// 数据没有变化时,跳过复杂的图表重绘
const processedData = processData(data); // 耗时计算
return <Chart data={processedData} />;
});
2.2 不值得使用 memo 的场景
场景一:组件本身渲染很快
// 一个简单的按钮组件,渲染时间 < 1ms
// memo 的比较开销可能比节省的渲染时间还大
function SimpleButton({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
// 不要 memo 化这个组件——不值得
场景二:Props 每次都创建新引用
function Parent() {
return (
<>
{/* 每次渲染都创建新对象,memo 永远比较失败 */}
<Child style={{ color: 'red' }} />
{/* 每次渲染都创建新函数,memo 永远比较失败 */}
<Child onClick={() => doSomething()} />
</>
);
}
// 这种情况下 memo 化毫无意义,反而增加了比较开销
场景三:组件使用 Context,且 Context 值经常变化
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar /> {/* theme 变化时整个子树都重渲染 */}
</ThemeContext.Provider>
);
}
在这种情况下,memo 化 Toolbar 的子组件没有意义,因为 Context 变化会导致它们重新渲染。
2.3 性能测试的重要性
过早优化是万恶之源。在应用 memo 之前,应该先测量:
// 使用 React DevTools Profiler 测量
// 或使用 useEffect 测量渲染时间
function useRenderTimer(name) {
const lastRender = useRef(Date.now());
const renderCount = useRef(0);
useEffect(() => {
const now = Date.now();
const duration = now - lastRender.current;
if (duration > 16) { // 超过一帧
console.log(`${name} 渲染耗时: ${duration}ms`);
}
lastRender.current = now;
renderCount.current++;
});
return renderCount.current;
}
三、PureComponent 的 shouldComponentUpdate
3.1 什么是 PureComponent
PureComponent 是 React 提供的类组件基类,它自动对 props 和 state 进行浅比较,只有当它们确实发生变化时才重新渲染。
class UserList extends React.PureComponent {
render() {
// 只有 this.props.users 变化时才重新执行
return (
<ul>
{this.props.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
}
3.2 PureComponent vs Component
// Component:每次 props 变化或 setState 调用都会重新渲染
class UserList extends React.Component {
render() {
return <div>{this.props.title}</div>;
}
}
// PureComponent:自动浅比较 props 和 state
class UserList extends React.PureComponent {
render() {
return <div>{this.props.title}</div>;
}
}
PureComponent 的 shouldComponentUpdate 自动做的是:
// PureComponent 等价于手动实现:
class UserList extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// 浅比较 props
const propsKeys = Object.keys(this.props);
for (const key of propsKeys) {
if (this.props[key] !== nextProps[key]) {
return true;
}
}
// 浅比较 state
const stateKeys = Object.keys(this.state);
for (const key of stateKeys) {
if (this.state[key] !== nextState[key]) {
return true;
}
}
return false;
}
render() {
return <div>{this.props.title}</div>;
}
}
3.3 函数组件的 memo vs 类组件的 PureComponent
React.memo 在函数组件上做的事情,本质上与 PureComponent 在类组件上做的事情相同:浅比较 props,决定是否跳过渲染。
// 类组件
class UserList extends React.PureComponent {
render() { /* ... */ }
}
// 函数组件(等价)
const UserList = React.memo(function UserList({ users }) {
return /* ... */;
});
选择建议:
- 新代码:优先使用函数组件 +
React.memo - 遗留代码:类组件可以用
PureComponent替代Component
四、自定义比较函数(第二个参数)
4.1 为什么需要自定义比较
默认的浅比较在某些场景下不够用:
// 默认比较无法检测对象内容的深层变化
const UserCard = React.memo(
function UserCard({ user }) {
return <div>{user.name} - {user.profile.bio}</div>;
},
(prevProps, nextProps) => {
// 自定义比较逻辑
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name
);
}
);
4.2 自定义比较的场景
场景一:只关心特定 props
const ProductCard = React.memo(
function ProductCard({ product, onAddToCart }) {
// 只关心 product 变化,不关心 onAddToCart
return (
<div>
<h3>{product.name}</h3>
<button onClick={onAddToCart}>添加</button>
</div>
);
},
(prevProps, nextProps) => {
return prevProps.product.id === nextProps.product.id;
}
);
场景二:复杂的比较逻辑
const DataTable = React.memo(
function DataTable({ columns, data, sortConfig }) {
return /* ... */;
},
(prevProps, nextProps) => {
// 检查关键 props 是否相等
return (
arraysEqual(prevProps.columns, nextProps.columns) &&
arraysEqual(prevProps.data, nextProps.data) &&
prevProps.sortConfig.key === nextProps.sortConfig.key &&
prevProps.sortConfig.direction === nextProps.sortConfig.direction
);
}
);
4.3 useMemo 在比较函数中的应用
有时候,你希望 memo 的 props 本身是稳定的引用——这时可以用 useMemo:
function Parent() {
const [count, setCount] = useState(0);
// columns 数组只有在 data 变化时才重新创建
const columns = useMemo(() => [
{ key: 'name', label: '名称' },
{ key: 'email', label: '邮箱' },
], []); // 空依赖,永久缓存
return (
<>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<DataTable columns={columns} data={data} />
{/* count 变化不会导致 columns 重新创建 */}
{/* DataTable 的 memo 比较会通过 */}
</>
);
}
五、memo 与 useMemo 的区别
5.1 功能定位不同
| 维度 | React.memo | useMemo |
|---|---|---|
| 本质 | HOC(包装组件) | Hook(缓存值) |
| 缓存内容 | 组件的渲染结果 | 任意计算结果 |
| 触发重新渲染条件 | props 变化 | 依赖数组中的值变化 |
| 使用场景 | 组件级别的渲染优化 | 值的计算缓存 |
5.2 React.memo 缓存组件渲染
const MemoizedComponent = React.memo(function Component({ data }) {
// 只有 data 引用变化时才重新渲染
return <ExpensiveRender data={data} />;
});
5.3 useMemo 缓存计算结果
function Component({ items, filter }) {
// 只有 items 或 filter 变化时才重新计算
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(filter));
}, [items, filter]);
return <List items={filteredItems} />;
}
5.4 两者的配合使用
function ProductList({ products, category }) {
// useMemo 缓存过滤后的结果
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === category);
}, [products, category]);
// React.memo 缓存列表项组件的渲染结果
return (
<div>
{filteredProducts.map(product => (
<MemoizedProductCard
key={product.id}
product={product}
/>
))}
</div>
);
}
const MemoizedProductCard = React.memo(function ProductCard({ product }) {
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}</p>
</div>
);
});
5.5 常见错误:混淆使用
// 错误:useMemo 用于组件
function Component({ data }) {
const MemoizedComponent = useMemo(
() => <ExpensiveComponent data={data} />,
[data]
);
return MemoizedComponent; // 返回的是 JSX,不是组件
}
// 正确:React.memo 用于组件
const MemoizedComponent = React.memo(function ExpensiveComponent({ data }) {
return <div>{data}</div>;
});
function Component({ data }) {
return <MemoizedComponent data={data} />;
}
六、避免不必要的 memo:过早优化的代价
6.1 memo 不是免费的
memo 有以下成本:
- 比较开销:每次渲染都需要比较 props,对于简单组件可能比渲染本身还贵
- 内存开销:需要存储上一次的渲染结果
- 代码复杂性:增加代码理解的难度
// 一个简单组件
function SimpleDiv({ text }) {
return <div>{text}</div>;
}
// memo 化后的比较成本
// 每次渲染:Object.keys(props) + 浅比较
// 简单组件的渲染成本:< 0.1ms
// memo 的比较成本:~0.05ms
// 得不偿失!
6.2 判断标准
值得 memo 的场景:
- 组件渲染时间 > 1ms
- 组件在频繁更新的父组件下
- 组件有大量子组件
不值得 memo 的场景:
- 简单展示组件(渲染 < 0.5ms)
- Props 每次都创建新引用
- 组件本身就因为 Context 或 state 变化而频繁更新
6.3 更好的优化思路
优化数据流,而不是依赖 memo:
// 不好的做法:依赖 memo 来阻止不必要的渲染
const Child = React.memo(function Child({ data }) {
return <div>{data.name}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
// data 每次都是新引用
const data = { name: 'John' };
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Child data={data} />
</>
);
}
// 好的做法:让 data 稳定
function Parent() {
const [count, setCount] = useState(0);
// data 保持稳定引用
const data = useMemo(() => ({ name: 'John' }), []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Child data={data} />
</>
);
}
6.4 useCallback 和 useMemo 的配合
当 props 是函数时,配合 useCallback 可以让 memo 更有效:
function Parent() {
const [count, setCount] = useState(0);
// useCallback 确保这个函数引用稳定
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{/* handleClick 引用稳定,MemoizedButton 不会被不必要的重新渲染 */}
<MemoizedButton onClick={handleClick} />
</>
);
}
七、面试高频问题
Q: React.memo 和 useMemo 的区别是什么?
回答要点:React.memo 是 HOC,用于包装组件,使其在 props 没变化时跳过渲染,缓存的是组件的渲染结果。useMemo 是 Hook,用于缓存任意计算结果,缓存的是值本身,不是组件。选择标准:组件渲染优化用 React.memo,值计算缓存用 useMemo。两者可以配合使用——用 useMemo 稳定 props 的引用,用 React.memo 防止 props 没变化时重新渲染。
Q: 什么时候不应该用 React.memo?
回答要点:不应该用 memo 的场景包括:组件渲染本身很快(memo 比较开销可能更大)、props 每次都是新引用(memo 比较永远失败)、组件本身因为 Context 或 state 变化而频繁更新(memo 形同虚设)。过早优化是常见错误,应该先测量再优化。
Q: PureComponent 和 React.memo 有什么区别?
回答要点:两者做的事情本质上相同——浅比较 props/state,决定是否重新渲染。区别是 PureComponent 用于类组件,通过重写 shouldComponentUpdate 实现;React.memo 用于函数组件,是 HOC 形式。在现代 React 开发中,函数组件 + React.memo 是推荐做法,PureComponent 主要用于遗留类组件的优化。
延展阅读
- React 官方文档:React.memo — 官方对 React.memo 的完整说明
- React 官方文档:PureComponent — 官方对 PureComponent 的说明
- Kent C. Dodds: React's useMemo and useCallback — 深入讲解 useMemo 和 useCallback 的使用场景
- Overreacted: Memo is not about performance — Dan Abramov 讲解 memo 真正解决的是什么
- React Docs: Optimizing Performance — 官方性能优化指南