memo 让组件在 props 与上次相同时跳过重新渲染。把组件包在 memo(...) 中,当它的父组件渲染时,React 会先逐个比较新旧 prop;如果全部相等,就复用上一次的结果,而不再调用这个组件。
在输入框中打字:输入很流畅,Console 中的 render SlowList 只出现一次,来自第一次渲染。去掉 memo( 包裹(以及对应的右括号)再打字:现在每次按键都要等列表 150 毫秒,并打印一次渲染。
语法
import { memo } from 'react';
const MemoizedComponent = memo(SomeComponent, arePropsEqual?);
SomeComponent可以是任何函数组件。常见写法是直接包裹定义:const Card = memo(function Card(props) { ... })。给内部函数命名,能让名字保留在 React DevTools 和错误信息中。arePropsEqual是可选的,很少需要(见下文)。- 返回值是一个组件,像其他组件一样渲染即可。
默认情况下,React 对新旧 props 做浅比较:用 Object.is 检查每个 prop。字符串、数字和布尔值按值比较,所以 title="Hi" 等于 title="Hi"。对象、数组和函数按引用比较:内容相同的两个对象仍然是不同的对象。
在上面的示例中,items 声明在组件外部,所以每次渲染都是同一个数组,比较能够通过。
新的对象和函数会让 memo 失效
在父组件主体中以字面量写出的东西,每次渲染都会重新创建:{ name: 'Ada' }、[1, 2, 3]、() => save()。把它们传给 memo 组件,这个 prop 就永远不等于之前的值。
每次点击都会打印 render Badge object prop,但字符串版本从不打印。修复对象这种情况有三种方法:
- 传入基本类型的值。当子组件只需要一两个字段时,用
name="Ada"代替user={{ name: 'Ada' }}。 - 把常量移到组件外部。永远不变的对象可以放在模块级别,就像第一个示例中的
items。 - 缓存它。由 props 或 state 构建的对象和数组用 useMemo 包裹,函数用 useCallback 包裹。
children 也有同样的问题。<Card><p>Hello</p></Card> 每次渲染都会创建一个新的 <p> 元素对象,所以接收 children 的 memo Card 每次都会重新渲染。
memo 拦不住的渲染
memo 只看父组件传来的 props。被记忆化的组件在以下情况仍然会渲染:
- 它自己的 state 变化,通过它自己的
useState或useReducer; - 它读取的 context 变化,通过
useContext。
点击 Unrelated,Panel 被跳过。切换主题,它会渲染,因为它读取了 context。点击 Like,它也会渲染,因为它自己的 state 变了。这正是预期的行为:memo 是在组件所依赖的东西都没变时跳过工作,而 context 和 state 都是它所依赖的东西。
自定义比较函数
第二个参数会替换默认的浅比较。它接收之前和之后的 props,当两者应被视为相等时返回 true,也就是让 React 跳过渲染。
const MemoChart = memo(Chart, (prev, next) => {
return (
prev.points.length === next.points.length &&
prev.points.every((p, i) => p === next.points[i]) &&
prev.color === next.color &&
prev.onSelect === next.onSelect
);
});
你很少需要它,而且很容易写错。如果函数忽略了某个 prop,组件就会一直显示这个 prop 的旧值。忽略函数类型的 prop 是最糟糕的情况:被跳过的组件会一直调用旧函数,而旧函数读到的是旧的 state。要比较每一个 prop,也不要对大量数据做深比较,否则比较的开销会超过它省下的渲染。通过 useMemo 和 useCallback 让 props 保持稳定,几乎总是更好的解决办法。
检查 memo 是否生效
最快的检查方法就是本页使用的方法:在组件顶部写一个 console.log。如果在你预期跳过时它打印了,就把 props 也打印出来,与之前的比较,找出变化的那一个。在实际项目中,React DevTools 能帮上更多忙:在它的设置中打开“Highlight updates when components render”,就能看到每次交互时哪些组件在闪烁;Profiler 标签页会记录每一次渲染,打开对应的设置后,还能看到每个组件渲染的原因。
有两种情况看起来像是 memo 失效了,其实不是:
- 第一次渲染总会发生。
memo只会在组件用这些 props 渲染过一次之后才跳过渲染。 - 开发构建可能渲染更多次。在 StrictMode 下,React 会在开发环境中把每个组件渲染两次,以发现不纯的代码。这里的预览按生产构建运行,所以上面的每一行日志都代表一次真实的渲染。
什么时候使用 memo
当一个组件渲染很慢,而它的父组件经常以相同的 props 渲染它时,memo 才值得用:输入框旁边的长列表或图表,每秒更新一次的页面中的侧边栏。对于渲染很快的组件,或者 props 每次都会变化的组件,它只会多一次比较,没有任何收益。在使用记忆化之前,先看看能否调整结构,比如把 state 往下移,或者把 JSX 作为 children 传入,详见渲染页面。
如果你的项目使用了 React Compiler 1.0,它会在构建时自动记忆化组件及其 props,大部分手动调用 memo 都变得不再必要。没有它时,memo 的行为与这里描述的完全一致,在 React 代码中依然很常见。
常见问题
React.memo 有什么作用?
它返回组件的一个记忆化版本。父组件渲染时,React 会把新的 props 与之前的比较,如果每个 prop 按 Object.is 都相等,就跳过这个组件的渲染。
为什么我的 memo 组件还是会重新渲染?
通常是因为某个 prop 是父组件渲染时新创建的对象、数组或函数,所以它永远不等于之前的值。另外,无论有没有 memo,组件自己的 state 变化或它读取的 context 变化时,它也会渲染。
React.memo 和 useMemo 有什么区别?
memo 包裹一个组件,跳过它的渲染。useMemo 是组件内部的 hook,用来缓存计算出的值。两者经常配合使用:useMemo 让对象类型的 prop 保持稳定,memo 才能跳过渲染。
应该给每个组件都包上 memo 吗?
不应该。大多数渲染开销都很小,而比较 props 本身也有开销。只对渲染慢、又经常收到相同 props 的组件使用它。
memo 对 children 有效吗?
很少有效。作为 children 传入的 JSX 每次渲染都是一个新对象,所以接收 children 的 memo 组件几乎总会重新渲染。