memo 允许你在组件的 props 未改变时跳过重新渲染。
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)参考
🌐 Reference
memo(Component, arePropsEqual?)
将组件封装在 memo 中以获得该组件的记忆化版本。当父组件重新渲染时,只要组件的 props 没有变化,这个记忆化版本的组件通常不会重新渲染。但 React 仍可能会重新渲染它:记忆化是一种性能优化,而不是保证。
🌐 Wrap a component in memo to get a memoized version of that component. This memoized version of your component will usually not be re-rendered when its parent component is re-rendered as long as its props have not changed. But React may still re-render it: memoization is a performance optimization, not a guarantee.
import { memo } from 'react';
const SomeComponent = memo(function SomeComponent(props) {
// ...
});参数
🌐 Parameters
Component:你想要记忆化的组件。memo不会修改此组件,而是返回一个新的记忆化组件。任何有效的 React 组件,包括函数和forwardRef组件,都可以接受。- 可选
arePropsEqual:一个接受两个参数的函数:组件的之前 props 和它的新 props。如果旧 props 和新 props 相等,也就是说,如果组件在使用新 props 时将产生与使用旧 props 相同的输出并以相同的方式工作,它应该返回true。否则,它应该返回false。通常,你不会指定此函数。默认情况下,React 会使用Object.is比较每个 prop。
返回
🌐 Returns
memo 返回一个新的 React 组件。它的行为与提供给 memo 的组件相同,除了当父组件重新渲染时,除非其 props 发生变化,否则 React 不会总是重新渲染它。
用法
🌐 Usage
属性不变时跳过重新渲染
🌐 Skipping re-rendering when props are unchanged
React 通常在其父组件重新渲染时重新渲染一个组件。使用 memo,你可以创建一个组件,只要其新的 props 与旧的 props 相同,React 就不会在其父组件重新渲染时重新渲染它。这样的组件被称为记忆化组件。
🌐 React normally re-renders a component whenever its parent re-renders. With memo, you can create a component that React will not re-render when its parent re-renders so long as its new props are the same as the old props. Such a component is said to be memoized.
要对组件进行记忆化,请将其封装在 memo 中,并使用它返回的值替代你的原始组件:
🌐 To memoize a component, wrap it in memo and use the value that it returns in place of your original component:
const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});
export default Greeting;一个 React 组件应始终具有纯渲染逻辑。这意味着如果其 props、state 和 context 没有改变,它必须返回相同的输出。通过使用 memo,你是在告诉 React 你的组件符合这一要求,因此只要 props 没有改变,React 就不需要重新渲染。即使使用 memo,如果组件自身的 state 发生变化或它使用的 context 发生变化,组件仍会重新渲染。
🌐 A React component should always have pure rendering logic. This means that it must return the same output if its props, state, and context haven’t changed. By using memo, you are telling React that your component complies with this requirement, so React doesn’t need to re-render as long as its props haven’t changed. Even with memo, your component will re-render if its own state changes or if a context that it’s using changes.
在这个例子中,请注意,当 name 发生变化时,Greeting 组件会重新渲染(因为它是其中一个属性),但当 address 发生变化时不会重新渲染(因为它没有作为属性传递给 Greeting):
🌐 In this example, notice that the Greeting component re-renders whenever name is changed (because that’s one of its props), but not when address is changed (because it’s not passed to Greeting as a prop):
import { memo, useState } from 'react'; export default function MyApp() { const [name, setName] = useState(''); const [address, setAddress] = useState(''); return ( <> <label> Name{': '} <input value={name} onChange={e => setName(e.target.value)} /> </label> <label> Address{': '} <input value={address} onChange={e => setAddress(e.target.value)} /> </label> <Greeting name={name} /> </> ); } const Greeting = memo(function Greeting({ name }) { console.log("Greeting was rendered at", new Date().toLocaleTimeString()); return <h3>Hello{name && ', '}{name}!</h3>; });
深入研究
🌐 Should you add memo everywhere?
如果你的应用像这个网站一样,并且大多数交互是粗略的(比如替换一个页面或整个部分),通常不需要记忆化。另一方面,如果你的应用更像一个绘图编辑器,并且大多数交互是细粒度的(比如移动形状),那么你可能会发现记忆化非常有用。
🌐 If your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful.
只有当你的组件在接收到完全相同的 props 时频繁重新渲染,并且其重新渲染的逻辑代价很高时,使用 memo 才有价值。如果组件重新渲染时没有明显的延迟,memo 就没有必要。请记住,如果传递给组件的 props 总是不同的,比如你在渲染时传递了一个对象或普通函数,那么 memo 完全没用。这就是为什么你通常需要将 useMemo 和 useCallback 与 memo 一起使用的原因。
🌐 Optimizing with memo is only valuable when your component re-renders often with the same exact props, and its re-rendering logic is expensive. If there is no perceptible lag when your component re-renders, memo is unnecessary. Keep in mind that memo is completely useless if the props passed to your component are always different, such as if you pass an object or a plain function defined during rendering. This is why you will often need useMemo and useCallback together with memo.
在其他情况下,将组件封装在 memo 中没有什么好处。这么做也没有显著的坏处,所以有些团队选择不去考虑具体情况,而尽可能进行 memoization。这种方法的缺点是代码的可读性会降低。此外,并非所有的 memoization 都有效:一个“总是新的”单一值就足以破坏整个组件的 memoization。
🌐 There is no benefit to wrapping a component in memo in other cases. There is no significant harm to doing that either, so some teams choose to not think about individual cases, and memoize as much as possible. The downside of this approach is that code becomes less readable. Also, not all memoization is effective: a single value that’s “always new” is enough to break memoization for an entire component.
在实践中,通过遵循几个原则,你可以使大量的 memo 化变得不必要:
- 当一个组件在视觉上封装其他组件时,让它接受 JSX 作为子组件。 这样,当封装组件更新自身状态时,React 就知道其子组件不需要重新渲染。
- 优先使用本地状态,并且不要将状态提升得比必要的更高。例如,不要将像表单或某个项目是否被悬停这样的临时状态保存在树的顶端或全局状态库中。
- 保持你的渲染逻辑纯粹。 如果重新渲染一个组件导致问题或产生一些明显的视觉瑕疵,那就是你的组件中的一个错误!修复这个错误,而不是添加记忆化。
- 避免不必要的会更新状态的效果。 大多数 React 应用的性能问题都是由效果引起的一系列更新造成的,这些更新会导致组件不断重新渲染。
- 尝试从你的 Effect 中移除不必要的依赖。 例如,与其使用记忆化,通常更简单的方法是将某些对象或函数移动到 Effect 内部或组件外部。
如果某个具体的交互仍然感觉有延迟,使用 React 开发者工具的 profiler 来查看哪些组件最能从 memoization 中受益,并在需要的地方添加 memoization。这些原则使你的组件更容易调试和理解,所以无论如何遵循它们都是好的。从长远来看,我们正在研究自动进行细粒度的 memoization,以一次性彻底解决这个问题。
🌐 If a specific interaction still feels laggy, use the React Developer Tools profiler to see which components would benefit the most from memoization, and add memoization where needed. These principles make your components easier to debug and understand, so it’s good to follow them in any case. In the long term, we’re researching doing granular memoization automatically to solve this once and for all.
使用状态更新记忆化组件
🌐 Updating a memoized component using state
即使一个组件被记忆化,当它自身的状态改变时,它仍然会重新渲染。记忆化仅与从父组件传递给该组件的 props 有关。
🌐 Even when a component is memoized, it will still re-render when its own state changes. Memoization only has to do with props that are passed to the component from its parent.
import { memo, useState } from 'react'; export default function MyApp() { const [name, setName] = useState(''); const [address, setAddress] = useState(''); return ( <> <label> Name{': '} <input value={name} onChange={e => setName(e.target.value)} /> </label> <label> Address{': '} <input value={address} onChange={e => setAddress(e.target.value)} /> </label> <Greeting name={name} /> </> ); } const Greeting = memo(function Greeting({ name }) { console.log('Greeting was rendered at', new Date().toLocaleTimeString()); const [greeting, setGreeting] = useState('Hello'); return ( <> <h3>{greeting}{name && ', '}{name}!</h3> <GreetingSelector value={greeting} onChange={setGreeting} /> </> ); }); function GreetingSelector({ value, onChange }) { return ( <> <label> <input type="radio" checked={value === 'Hello'} onChange={e => onChange('Hello')} /> Regular greeting </label> <label> <input type="radio" checked={value === 'Hello and welcome'} onChange={e => onChange('Hello and welcome')} /> Enthusiastic greeting </label> </> ); }
如果你将状态变量设置为它当前的值,即使没有 memo,React 也会跳过重新渲染你的组件。你可能仍会看到你的组件函数被调用多一次,但结果将被丢弃。
🌐 If you set a state variable to its current value, React will skip re-rendering your component even without memo. You may still see your component function being called an extra time, but the result will be discarded.
使用上下文更新记忆化组件
🌐 Updating a memoized component using a context
即使组件被记忆化,当它使用的上下文发生变化时,它仍然会重新渲染。记忆化只与从其父组件传递给组件的属性有关。
🌐 Even when a component is memoized, it will still re-render when a context that it’s using changes. Memoization only has to do with props that are passed to the component from its parent.
import { createContext, memo, useContext, useState } from 'react'; const ThemeContext = createContext(null); export default function MyApp() { const [theme, setTheme] = useState('dark'); function handleClick() { setTheme(theme === 'dark' ? 'light' : 'dark'); } return ( <ThemeContext value={theme}> <button onClick={handleClick}> Switch theme </button> <Greeting name="Taylor" /> </ThemeContext> ); } const Greeting = memo(function Greeting({ name }) { console.log("Greeting was rendered at", new Date().toLocaleTimeString()); const theme = useContext(ThemeContext); return ( <h3 className={theme}>Hello, {name}!</h3> ); });
为了让你的组件仅在某部分上下文发生变化时重新渲染,请将组件拆分为两个。在外部组件中读取你需要的上下文,并将其作为属性传递给 memoized 的子组件。
🌐 To make your component re-render only when a part of some context changes, split your component in two. Read what you need from the context in the outer component, and pass it down to a memoized child as a prop.
最小化属性变化
🌐 Minimizing props changes
当你使用 memo 时,只要任何 prop 与之前的不浅比较不相等,你的组件就会重新渲染。这意味着 React 会使用 Object.is 比较来比较组件中的每个 prop 与其之前的值。请注意,Object.is(3, 3) 是 true,但 Object.is({}, {}) 是 false。
🌐 When you use memo, your component re-renders whenever any prop is not shallowly equal to what it was previously. This means that React compares every prop in your component with its previous value using the Object.is comparison. Note that Object.is(3, 3) is true, but Object.is({}, {}) is false.
为了充分利用 memo,应尽量减少 props 的变化次数。例如,如果 prop 是一个对象,可以使用 useMemo: 来防止父组件每次都重新创建该对象。
🌐 To get the most out of memo, minimize the times that the props change. For example, if the prop is an object, prevent the parent component from re-creating that object every time by using useMemo:
function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
const person = useMemo(
() => ({ name, age }),
[name, age]
);
return <Profile person={person} />;
}
const Profile = memo(function Profile({ person }) {
// ...
});减少 props 变化的更好方法是确保组件在其 props 中只接受最必要的信息。例如,它可以接受单个值而不是一个完整的对象:
🌐 A better way to minimize props changes is to make sure the component accepts the minimum necessary information in its props. For example, it could accept individual values instead of a whole object:
function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}
const Profile = memo(function Profile({ name, age }) {
// ...
});即使是单个值有时也可以投射为变化较少的值。例如,这里一个组件接受一个表示值是否存在的布尔值,而不是值本身:
🌐 Even individual values can sometimes be projected to ones that change less frequently. For example, here a component accepts a boolean indicating the presence of a value rather than the value itself:
function GroupsLanding({ person }) {
const hasGroups = person.groups !== null;
return <CallToAction hasGroups={hasGroups} />;
}
const CallToAction = memo(function CallToAction({ hasGroups }) {
// ...
});当你需要将一个函数传递给记忆化组件时,要么在组件外部声明它,以确保它永远不会改变,要么useCallback在重新渲染之间缓存它的定义。
🌐 When you need to pass a function to memoized component, either declare it outside your component so that it never changes, or useCallback to cache its definition between re-renders.
指定自定义比较函数
🌐 Specifying a custom comparison function
在极少数情况下,最小化一个 memoized 组件的 props 变化可能是不可行的。在这种情况下,你可以提供一个自定义比较函数,React 将使用它来比较旧的和新的 props,而不是使用浅比较。该函数作为第二个参数传递给 memo。只有当新的 props 将产生与旧的 props 相同的输出时,它才应返回 true;否则应返回 false。
🌐 In rare cases it may be infeasible to minimize the props changes of a memoized component. In that case, you can provide a custom comparison function, which React will use to compare the old and new props instead of using shallow equality. This function is passed as a second argument to memo. It should return true only if the new props would result in the same output as the old props; otherwise it should return false.
const Chart = memo(function Chart({ dataPoints }) {
// ...
}, arePropsEqual);
function arePropsEqual(oldProps, newProps) {
return (
oldProps.dataPoints.length === newProps.dataPoints.length &&
oldProps.dataPoints.every((oldPoint, index) => {
const newPoint = newProps.dataPoints[index];
return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
})
);
}如果你这样做,请使用浏览器开发者工具中的性能面板,确保你的比较函数实际上比重新渲染组件更快。你可能会感到惊讶。
🌐 If you do this, use the Performance panel in your browser developer tools to make sure that your comparison function is actually faster than re-rendering the component. You might be surprised.
当你进行性能测量时,请确保 React 在生产模式下运行。
🌐 When you do performance measurements, make sure that React is running in the production mode.
如果我使用 React Compiler,我还需要 React.memo 吗?
🌐 Do I still need React.memo if I use React Compiler?
当你启用 React 编译器 时,通常不再需要 React.memo。编译器会自动为你优化组件的重新渲染。
🌐 When you enable React Compiler, you typically don’t need React.memo anymore. The compiler automatically optimizes component re-rendering for you.
工作原理:
🌐 Here’s how it works:
没有 React 编译器,你需要 React.memo 来防止不必要的重新渲染:
// Parent re-renders every second
function Parent() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<>
<h1>Seconds: {seconds}</h1>
<ExpensiveChild name="John" />
</>
);
}
// Without memo, this re-renders every second even though props don't change
const ExpensiveChild = memo(function ExpensiveChild({ name }) {
console.log('ExpensiveChild rendered');
return <div>Hello, {name}!</div>;
});启用 React 编译器 后,相同的优化会自动发生:
// No memo needed - compiler prevents re-renders automatically
function ExpensiveChild({ name }) {
console.log('ExpensiveChild rendered');
return <div>Hello, {name}!</div>;
}以下是 React 编译器生成的关键部分:
🌐 Here’s the key part of what the React Compiler generates:
function Parent() {
const $ = _c(7);
const [seconds, setSeconds] = useState(0);
// ... other code ...
let t3;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <ExpensiveChild name="John" />;
$[4] = t3;
} else {
t3 = $[4];
}
// ... return statement ...
}注意高亮的行:编译器在缓存检查中封装了 <ExpensiveChild name="John" />。由于 name 属性始终是 "John",这个 JSX 只会创建一次,并在每次父组件重新渲染时重复使用。这正是 React.memo 的作用——当子组件的属性没有改变时,它可以防止子组件重新渲染。
🌐 Notice the highlighted lines: The compiler wraps <ExpensiveChild name="John" /> in a cache check. Since the name prop is always "John", this JSX is created once and reused on every parent re-render. This is exactly what React.memo does - it prevents the child from re-rendering when its props haven’t changed.
React 编译器会自动:
🌐 The React Compiler automatically:
- 跟踪传递给
ExpensiveChild的name属性是否没有变化 - 重用之前为
<ExpensiveChild name="John" />创建的 JSX - 完全跳过重新渲染
ExpensiveChild
这意味着 在使用 React 编译器时,你可以安全地从组件中移除 React.memo。编译器会自动提供相同的优化,使你的代码更简洁、更易于维护。
🌐 This means you can safely remove React.memo from your components when using React Compiler. The compiler provides the same optimization automatically, making your code cleaner and easier to maintain.
故障排除
🌐 Troubleshooting
当属性是对象、数组或函数时,我的组件会重新渲染
🌐 My component re-renders when a prop is an object, array, or function
React 通过浅层相等性比较旧的和新的 props:也就是说,它会考虑每个新的 prop 是否与旧的 prop 引用相同。如果每次父组件重新渲染时你都创建一个新的对象或数组,即使其中的各个元素相同,React 仍然会认为它已更改。同样,如果在渲染父组件时创建一个新函数,即使该函数定义相同,React 也会认为它已更改。为了避免这种情况,简化 props 或在父组件中对 props 进行记忆化。
🌐 React compares old and new props by shallow equality: that is, it considers whether each new prop is reference-equal to the old prop. If you create a new object or array each time the parent is re-rendered, even if the individual elements are each the same, React will still consider it to be changed. Similarly, if you create a new function when rendering the parent component, React will consider it to have changed even if the function has the same definition. To avoid this, simplify props or memoize props in the parent component.