前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过防止不必要的重新渲染来优化 React 性能

通过防止不必要的重新渲染来优化 React 性能

作者头像
小弟调调
发布2022-11-08 13:04:41
6K0
发布2022-11-08 13:04:41
举报
文章被收录于专栏:埋名埋名

Re-rendering React components unnecessarily can slow down your app and make the UI feel unresponsive.

不必要地重新渲染 React 组件会减慢您的应用程序并让 UI 感觉没有响应。

This article explains how to update components only when necessary, and how to avoid common causes of unintentional re-renders. 本文解释了如何仅在必要时更新组件,以及如何避免意外重新渲染的常见原因。

Use React.memo or React.PureComponent

(使用 React.memoReact.PureComponent)

When a component re-renders, React will also re-render child components by default. 当一个组件重新渲染时,React 默认也会重新渲染子组件。

Here's a simple app with two Counter components and a button that increments one of them. 这是一个简单的应用程序,它有两个 Counter 组件和一个递增其中一个的按钮。

代码语言:javascript
复制
function App() {
  const [counterA, setCounterA] = React.useState(0);
  const [counterB, setCounterB] = React.useState(0);

  return (
    <div>
      <Counter name="A" value={counterA} />
      <Counter name="B" value={counterB} />
      <button
        onClick={() => {
          console.log("Click button");
          setCounterA(counterA + 1);
        }}
      >
        Increment counter A
      </button>
    </div>
  );
}
function Counter({ name, value }) {
  console.log(`Rendering counter ${name}`);
  return (
    <div>
      {name}: {value}
    </div>
  );
}

Right now, both Counter components render when the button is clicked, even though only counter A has changed. 现在,当单击按钮时,两个 Counter 组件都会呈现,即使只有计数器 A 发生了变化。

代码语言:javascript
复制
Click button
Rendering counter A
Rendering counter B

The React.memo higher-order component (HOC) can ensure a component is only re-rendered when its props change. React.memo 高阶组件 (HOC) 可以确保组件仅在其 props 更改时才重新渲染。

代码语言:javascript
复制
const Counter = React.memo(function Counter({ name, value }) {
  console.log(`Rendering counter ${name}`);
  return (
    <div>
      {name}: {value}
    </div>
  );
});

Now only counter A is re-rendered, because it's value prop changed from 0 to 1. 现在只有计数器 A 被重新渲染,因为它的 value 属性从 0 更改为 1

代码语言:javascript
复制
Click button
Rendering counter A

For class-based components

(对于 class-based 的组件)

If you're using class-based components instead of function components, change extends React.Component to extends React.PureComponent to get the same effect. 如果您使用基于类的组件而不是函数组件,请将 extends React.Component 更改为 extends React.PureComponent 以获得相同的效果。

Make sure property values don't change

(确保属性值不会改变)

Preventing the render in our example was pretty easy. But in practice this is more difficult, as it's easy for unintentional prop changes to sneak in. 在我们的示例中阻止渲染非常简单。 但在实践中,这更加困难,因为无意的道具更改很容易潜入。

Let's include the Increment button in the Counter component. 让我们在 Counter 组件中包含增量按钮。

代码语言:javascript
复制
const Counter = React.memo(function Counter({ name, value, onClickIncrement }) {
  console.log(`Rendering counter ${name}`);
  return (
    <div>
      {name}: {value} <button onClick={onClickIncrement}>Increment</button>
    </div>
  );
});

The App component now passes in an onClickIncrement prop to each Counter. App 组件现在将 onClickIncrement 属性传递给每个 Counter

代码语言:javascript
复制
<Counter
  name="A"
  value={counterA}
  onClickIncrement={() => setCounterA(counterA + 1)}
/>

If you increment counter A, both counters are re-rendered. 如果增加计数器 A,两个计数器都会重新渲染。

代码语言:javascript
复制
Rendering counter A
Rendering counter B

Why? Because the value of the onClickIncrement prop changes every time the app re-renders. Each function is a distinct JavaScript object, so React sees the prop change and makes sure to update the Counter.为什么? 因为每次应用重新渲染时,onClickIncrement 属性的值都会改变。 每个函数都是一个不同的 JavaScript 对象,因此 React 会看到 prop 更改并确保更新 Counter。

This makes sense, because the onClickIncrement function depends on the counterA value from its parent scope. If the same function was passed into the Counter every time, then the increment would stop working as the initial counter value would never update. The counter value would be set to 0 + 1 = 1 every time. 这是有道理的,因为 onClickIncrement 函数依赖于其父作用域中的 counterA 值。 如果每次都将相同的函数传递给“计数器”,那么增量将停止工作,因为初始计数器值永远不会更新。 计数器值每次都会设置为“0 + 1 = 1”。

The problem is that the onClickIncrement function changes every time, even if the counter value it references hasn't changed. 问题是 onClickIncrement 函数每次都会改变,即使它引用的计数器值没有改变。

We can use the useCallback hook to fix this. useCallback memoizes the function that's passed in, so that a new function is only returned when one of the hook dependencies changes. 我们可以使用 useCallback 钩子来解决这个问题。 useCallback 会记住传入的函数,以便仅当挂钩依赖项之一发生更改时才返回新函数。

In this case the dependency is the counterA state. When this changes, the onClickIncrement function has to update, so that we don't use outdated state later on. 在这种情况下,依赖是 counterA 状态。 当这种情况发生变化时,onClickIncrement 函数必须更新,这样我们以后就不会使用过时的状态。

代码语言:javascript
复制
<Counter
  name="A"
  value={counterA}
  onClickIncrement={React.useCallback(() => setCounterA(counterA + 1), [
    counterA,
  ])}
/>

If we increment counter A now, only counter A re-renders. 如果我们现在增加计数器 A,则只有计数器 A 重新渲染。

代码语言:javascript
复制
Rendering counter A

For class-based components

(对于 class-based 的组件)

If you're using class-based components instead of function components, change extends `React.Component` to extends `React.PureComponent` to get the same effect. 如果您使用基于类的组件,请向类添加方法并在构造函数中使用 bind 函数以确保它可以访问组件实例。

代码语言:javascript
复制
constructor(props) {
  super(props)
  this.onClickIncrementA = this.onClickIncrementA.bind(this)
}

(You can't call bind in the render function, as it returns a new function object and would cause a re-render.) (您不能在渲染函数中调用 bind,因为它返回一个新的函数对象并会导致重新渲染。)

Passing objects as props

(将对象作为道具传递)

Unintentional re-renders not only happen with functions, but also with object literals. 无意的重新渲染不仅发生在函数中,还发生在对象字面量中。

代码语言:javascript
复制
function App() {
  return <Heading style={{ color: "blue" }}>Hello world</Heading>
}

Every time the App component renders a new style object is created, leading the memoized Heading component to update. 每次 App 组件渲染时都会创建一个新的样式对象,从而导致记忆中的 Heading 组件更新。

Luckily, in this case the style object is always the same, so we can just create it once outside the App component and then re-use it for every render. 幸运的是,在这种情况下,样式对象始终是相同的,因此我们可以在 App 组件之外创建一次,然后在每次渲染时重新使用它。

代码语言:javascript
复制
const headingStyle = { color: "blue" }
function App() {
  return <Heading style={headingStyle}>Hello world</Heading>
}

But what if the style is calculated dynamically? In that case you can use the useMemo hook to limit when the object is updated. 但是如果样式是动态计算的呢? 在这种情况下,您可以使用 useMemo 挂钩来限制对象的更新时间。

代码语言:javascript
复制
function App({ count }) {
   const headingStyle = React.useMemo(
    () => ({
      color: count < 10 ? "blue" : "red",
    }),
    [count < 10]
  );
  return <Heading style={headingStyle}>Hello world</Heading>
}

Note that the hook dependency is not the plain count, but the count < 10 condition. That way, if the count changes, the heading is only re-rendered if the color would change as well. 请注意,钩子依赖不是简单的计数,而是 count < 10 条件。 这样,如果计数发生变化,只有在颜色也发生变化时才会重新渲染标题。

children props

(子道具)

We get the same problems with object identity and unintentional re-renders if the children we pass in are more than just a simple string. 如果我们传入的子节点不仅仅是一个简单的字符串,我们会在对象标识和无意的重新渲染方面遇到同样的问题。

代码语言:javascript
复制
<Heading>
  <strong>Hello world</strong>
</Heading>

However, the same solutions apply. If the children are static, move them out of the function. If they depend on state, use useMemo. 然而,同样的解决方案也适用。 如果孩子是静态的,请将它们移出函数。 如果它们依赖于状态,请使用 useMemo

代码语言:javascript
复制
function App({}) {
  const content = React.useMemo(() => <strong>Hello world ({count}</strong>, [
    count,
  ]);

  return (
    <>
      <Heading>{content}</Heading>
    </>
  );
}

Using keys to avoid re-renders

(使用键(key)来避免重新渲染)

Key props allow React to identify elements across renders. They're most commonly used when rendering a list of items. Key props 允许 React 跨渲染识别元素。 它们最常用于渲染项目列表。

If each list element has a consistent key, React can avoid re-rendering components even when list items are added or removed. 如果每个列表元素都有一个一致的键,那么即使添加或删除列表项,React 也可以避免重新渲染组件。

代码语言:javascript
复制
function App() {
  console.log("Render App");
  const [items, setItems] = React.useState([{ name: "A" }, { name: "B" }]);
  return (
    <div>
      {items.map((item) => (
        <ListItem item={item} />
      ))}
      <button onClick={() => setItems(items.slice().reverse())}>Reverse</button>
    </div>
  );
}
const ListItem = React.memo(function ListItem({ item }) {
  console.log(`Render ${item.name}`);
  return <div>{item.name}</div>;
});

Without the key on <ListItem> we're getting a Warning: Each child in a list should have a unique "key" prop message. 如果没有 <ListItem> 上的键,我们会收到警告:列表中的每个孩子都应该有一个唯一的“键”道具消息。

This is the log output when clicking on the Reverse button. 这是单击“Reverse”按钮时的日志输出。

代码语言:javascript
复制
=> Reverse
Render app
Render B
Render A

Instead of moving the elements around, React instead updates both of them and passes in the new item prop. React 没有移动元素,而是更新它们并传入新的 item 属性。

Adding a unique key to each list item fixes the issue. 为每个列表项添加唯一键可解决此问题。

代码语言:javascript
复制
<ListItem item={item} key={item.name} />

React can now correctly recognize that the items haven't changed, and just moves the existing elements around. React 现在可以正确识别项目没有更改,并且只是移动现有元素。

What's a good key?

什么是好 key?

Keys should be unique, and no two elements in a list should have the same key. The item.name key we used above isn't ideal because of this, as multiple list elements might have the same name. Where possible, assign a unique ID to each list item – often you'll get this from the backend database. 键应该是唯一的,并且列表中的任何两个元素都不应具有相同的键。 我们上面使用的 item.name 键并不理想,因为多个列表元素可能具有相同的名称。 在可能的情况下,为每个列表项分配一个唯一的 ID——通常你会从后端数据库中得到这个。

Keys should also be stable. If you use Math.random() then the key will change every time, causing the component to re-mount and re-render.键也应该是稳定的。 如果您使用 Math.random(),那么每次都会更改密钥,从而导致组件重新挂载和重新渲染。

For static lists, where no items are added or removed, using the array index is also fine. 对于没有添加或删除项目的静态列表,使用数组索引也可以。

Keys on fragments

(Fragment 上的键)

You can't add keys to fragments using the short syntax (<>), but it works if you use the full name: 您不能使用短语法(<>)将键添加到片段,但如果您使用全名,它可以工作:

代码语言:javascript
复制
<React.Fragment key={item.name}>
</React.Fragment>

Avoid changes in the DOM tree structure

(避免更改 DOM 树结构)

Child components will be remounted if the surrounding DOM structure changes. For example, this app adds a container around the list. In a more realistic app you might put items in different groups based on a setting. 如果周围的 DOM 结构发生变化,子组件将被重新挂载。例如,这个应用程序在列表周围添加了一个容器。 在更现实的应用程序中,您可能会根据设置将项目放在不同的组中。

代码语言:javascript
复制
function App() {
  console.log("Render App");
  const [items, setItems] = React.useState([{ name: "A" }, { name: "B" }]);
  const [showContainer, setShowContainer] = React.useState(false);
  const els = items.map((item) => <ListItem item={item} key={item.name} />);
  return (
    <div>
      {showContainer > 0 ? <div>{els}</div> : els}
      <button onClick={() => setShowContainer(!showContainer)}>
        Toggle container
      </button>
    </div>
  );
}
const ListItem = React.memo(function ListItem({ item }) {
  console.log(`Render ${item.name}`);
  return <div>{item.name}</div>;
});

When the parent component is added all existing list items are unmounted and new component instances are created. React Developer Tools shows that this is the first render of the component. 添加父组件后,所有现有列表项都将被卸载并创建新的组件实例。 React Developer Tools 显示这是组件的第一次渲染。

React Developer Tools update because of first render React Developer Tools 更新,因为第一次渲染

Where possible, keep the DOM structure the same. For example, if you need to show dividers between groups within the list, insert the dividers between the list elements, instead of adding a wrapper div to each group. 在可能的情况下,保持 DOM 结构相同。 例如,如果您需要在列表中的组之间显示分隔符,请在列表元素之间插入分隔符,而不是为每个组添加包装 div。

Monitor the performance of your React app

(监控你的 React 应用程序的性能)

DebugBear can track the load time and CPU activity of your website over time. Just enter your URL to get started. DebugBear(https://www.debugbear.com/) 可以随着时间的推移跟踪您网站的加载时间和 CPU 活动。 只需输入您的 URL 即可开始。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-08-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JSdig 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Use React.memo or React.PureComponent
    • For class-based components
    • Make sure property values don't change
      • For class-based components
      • Passing objects as props
      • children props
      • Using keys to avoid re-renders
        • What's a good key?
          • Keys on fragments
          • Avoid changes in the DOM tree structure
          • Monitor the performance of your React app
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档