React re-render

羊羊羊 1年前 ⋅ 2095 阅读
ad

1. 什么是 re-render

每当提及 React 的性能时,我们需要特别关注下面两个渲染阶段:

  • initial render:组件第一次在页面上进行渲染

  • re-render:已经在页面上渲染的组件进行第二次或后续多次渲染

当页面更新数据的时候,React 组件就会发生 re-render,比如用户与页面之间产生交互、异步请求数据或者订阅的外部数据更新等这些场景都会导致 re-render。那些没有任何异步数据更新的非交互式应用程序永远不会发生 re-render,因此这种应用场景不需要关心 re-render 的性能优化。

哪些是必要或非必要的 re-render

必要的 re-render:组件发生重新渲染的原因是数据发生了变化,组件要把最新的数据渲染到页面上。例如,用户在输入框中输入文字,组件在每次按键时通过状态管理完成更新和渲染,对于这个组件来说这就是必要的 re-render。

不必要的 re-render:由于错误的实现方式,某个组件的 re-render 导致了整个页面全部重新渲染,这就是不必要的 re-render。比如,用户在输入框中输入文字,并且在每次按键时整个页面都进行了渲染,对于整个页面来说这就是非必要的 re-render。

非必要的 re-render 本身不存在问题:React 非常快速,通常能够在用户还未注意到的情况下处理它们。然而,如果 re-render 过于频繁或在非常重的组件上进行时,可能会让用户感觉到 “卡顿”,在交互过程中会出现明显延迟,甚至页面完全没有响应。

2. 什么时候 React 组件会发生 re-render

组件发生重新渲染有四个原因:状态更改、父级(或子级)重新渲染、context 变化以及 hooks 变化。这里有一个很大的误区:当组件的 props 改变时,组件会重新渲染。就其本身而言,这并不是真的(见本文后面的介绍)。

re-render 原因:状态变化

当组件的状态发生变化时,它将重新渲染自身。通常,它发生在回调或 useEffect 中。状态变化是所有重新渲染的根因。

re-render 原因:父组件重新渲染

如果组件的父组件重新渲染,则组件将重新渲染自身。反过来看也是对的:当组件重新渲染时,它也会重新渲染其所有子组件。但是,子组件的重新渲染不会触发父级的 re-render。

re-render 原因:context 变化

当 Context Provider 中的值发生变化时,使用该 Context 的所有组件都要 re-render,即使它们并没有使用发生变化的那部分数据。这些 re-render 并不能直接通过 memoize 来避免掉,但是可以用一些变通的方法来避免(参见第 7 部分:防止由 Context 引起的重新渲染)。

re-render 原因:hooks 变化

hooks 中发生的一切都 “属于” 使用它的组件。因此 Context 和 State 的更新规则同样也适用于这里:

  • hooks 内部的状态变化会触发组件的 re-render

  • 如果 hooks 使用了 context,并且 context 的值发生了变化,也会触发组件的 re-render

hooks 可以嵌套使用,其中每个 hooks 都 “属于” 使用它的组件,相同的规则适用于其中任何一个 hooks。

re-render 误区:props 变化

说到未被 memo 包裹的组件 re-render 时,组件的 props 是否发生变化并不重要。组件的 props 即便是发生了改变,也是由父组件来更新它们。也就是说,父组件的重新渲染触发了子组件的重新渲染,与子组件的 props 是否变化无关。只有那些使用了 React.memo 和 useMemo 的组件,props 的变化才会触发组件的重新渲染。

3. 避免 re-render:组件复合

反模式:在 render 函数中创建组件

在组件的渲染函数中创建一个组件是一种反模式,很有可能会引起性能问题。在每次重新渲染时,React 都会重新装载这个组件(即销毁它并从头开始重新创建),这比正常的重新渲染要慢得多。除此之外,还会导致以下问题:

  • 在重新渲染期间可能出现内容 “闪烁”

  • 每次重新渲染时在组件的状态会被重置

  • 每次重新渲染时不会触发依赖项的 useEffect

  • 如果组件被聚焦,则焦点将丢失

组件复合避免 re-render:state 下移到子组件中

这个模式非常有用,特别是对逻辑比较复杂的组件做状态管理时,并且这些状态仅仅用在了渲染树的一小部分上。一个典型的例子就是,在页面重要且复杂的组件中,实现单击按钮打开 / 关闭对话框。在这种情况下,控制对话框开关的状态可以封装在较小的组件中。这样,整个大组件就不会因为这些状态的更改而发生重新渲染。

组件复合避免 re-render:children 作为 props

这个模式与状态下移比较类似,也是将状态变化封装在较小的组件中。不同之处在于,这里的状态是作用于包裹复杂组件的父组件上,因此无法通过状态下移的方式作用于较小的组件。一个典型的例子是绑定到组件根元素上 onScroll 或 onMouseMove 回调。在这种场景下,可以将状态管理和使用该状态的组件提取到较小的组件中,并且可以将较复杂的组件作为 children props 传递给它。从较小的组件角度来看,children 只是 props,因此它们不会受到状态更改的影响,因此不会重新渲染。

组件复合避免 re-render:组件作为 props

与前面的模式基本相同,将状态封装在一个较小的组件中,而较重的组件作为 props 传递给它。props 不受 state 变化的影响,因此该较重的组件不会被重新渲染。这个模式适用于那些状态独立的复杂组件,并且不能抽离成一个 children 属性的场景。

4. 避免 re-render:使用 React.memo

用 React.memo 包装的组件会阻止重新渲染,除非这个组件的 props 发生了变化。这对于不依赖于重新渲染的组件,是非常有用的。

React.memo: 带有 props 的组件

对于非基础数据类型的 props 都要用 React.memo 包装成为 memoize 值。