React.memo 是一个高阶组件,用于阻止因父组件重渲染而导致的子组件不必要的重新渲染。它通过浅比较(shallow comparison)来判断组件的 Props 是否发生变化,如果 Props 没有变化,则复用上一次渲染的结果,跳过本次渲染。
为什么需要 React.memo
React 的默认行为是:当父组件重新渲染时,其内部的所有子组件都会递归地重新渲染。即使子组件接收的 Props 完全没有变化,也依然会触发渲染。这本身并非缺陷,但在大型应用中,频繁的无效渲染会累积成性能问题。
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Alice');
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child name={name} /> {/* 每次 count 变化,Child 都会重渲染 */}
</div>
);
}
function Child({ name }) {
console.log('Child 渲染了');
return <p>{name}</p>;
}
上述代码中,Child 的 name 属性并未改变,但每次点击按钮更新 count 时,Child 都会重新渲染并打印日志。如果 Child 内部有复杂计算或渲染大列表,就会产生可感知的卡顿。
基本用法
用 React.memo 包裹目标组件即可:
const Child = React.memo(function Child({ name }) {
console.log('Child 渲染了');
return <p>{name}</p>;
});
现在,只有当 name 真的发生变化时,Child 才会重新渲染。父组件因 count 改变而重渲染时,Child 会直接复用上一次的结果,不会执行组件函数。
浅比较的工作原理
React.memo 默认对 Props 中的每个字段进行 Object.is 比较(相当于 ===)。对于原始类型(字符串、数字、布尔值),只要值相同就不会触发渲染;对于引用类型(对象、数组、函数),相同的引用才会被视为未变化。
这意味着:如果父组件每次渲染都会生成新的对象或函数,那么 React.memo 将形同虚设。
// ❌ 父组件每次渲染都传递一个新的对象,memo 失效
<Child style={{ color: 'red' }} />
// ❌ 父组件每次渲染都传递一个新的函数
<Child onClick={() => handleClick()} />
要解决这个问题,需要配合 useMemo 和 useCallback 来稳定引用:
const style = useMemo(() => ({ color: 'red' }), []);
const handleClick = useCallback(() => { ... }, []);
<Child style={style} onClick={handleClick} />
自定义比较函数
如果默认的浅比较不满足需求,可以传入第二个参数——一个自定义比较函数。该函数返回 true 表示“Props 相等,不需要渲染”,返回 false 表示“Props 不相等,需要重渲染”。
const Child = React.memo(
function Child({ user }) {
return <p>{user.name}</p>;
},
(prevProps, nextProps) => {
// 只比较 user.id,忽略其他字段
return prevProps.user.id === nextProps.user.id;
}
);
注意:自定义比较函数会增加比较成本,只在确实需要更精细的控制时才使用。大多数情况下,配合 useMemo/useCallback 已经足够。
适用场景与反模式
何时使用 React.memo:
- 纯展示组件:组件只根据 Props 渲染,内部没有状态和副作用。这类组件是 memo 化最理想的目标。
- 渲染成本高的组件:组件内部有复杂计算、大列表渲染或深层嵌套的子组件树。
- 在同一父组件下渲染大量相同子组件:例如长列表中的每一项,memo 化可以避免整个列表的重渲染。
- Props 变化不频繁:如果组件的 Props 变化频率远低于父组件渲染频率,memo 能有效消除无效渲染。
何时不应使用 React.memo:
- Props 频繁变化:如果组件的 Props 在每次父组件渲染时几乎都会变化,memo 的比较成本会白白浪费,甚至因为每次都要比较后再决定渲染,反而比直接渲染更慢。
- 组件本身极轻量:比如一个简单的
<div>或只渲染几个字符的<span>,memo 带来的收益微乎其微,反而增加代码复杂度。 - 依赖全局 Context:
React.memo只监听 Props 变化,如果组件内部使用了useContext,Context 值变化时组件依然会重渲染(Context 变化不受 memo 控制)。
与其他优化手段的配合
React.memo 是组件层级的优化,需要和 Hooks 层级优化一起使用才能发挥最大效果:
- 状态内容提升:把变化频繁的状态下沉到子树中,减少顶层组件的重渲染扩散。
- children 模式:利用
children作为“免渲染”插槽,避免直接嵌套导致的渲染传播。 - 虚拟列表:对于超长列表,结合
react-window等方案,仅渲染可视区域。
// 状态下沉:把频繁变化的 count 封装在独立的计数器组件内
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// 稳定展示的列表组件,用 memo 包裹
const UserList = React.memo(function UserList({ users }) {
return users.map(u => <div key={u.id}>{u.name}</div>);
});
function App() {
return (
<div>
<Counter />
<UserList users={users} /> {/* users 不变就不会重渲染 */}
</div>
);
}
实际性能验证
不要凭直觉进行优化,务必使用 React DevTools 的 Profiler 面板来测量渲染耗时和渲染次数。勾选“Highlight updates when components render”选项,观察哪些组件进行了不必要的重渲染,再有针对性地应用 React.memo。过早优化是万恶之源,但要懂得在性能瓶颈出现时使用正确的工具。
总结
React.memo 是组件级性能优化的直接手段,核心原理是通过 Props 浅比较跳过无变化的渲染。它的有效性依赖于稳定的 Props 引用,通常需要与 useMemo、useCallback 协同工作。使用它要基于实际性能测量,优先优化渲染昂贵的组件,避免在轻量组件和频繁变化的组件上滥用。