一份详尽的 React re-render 指南

476次阅读  |  发布于2年以前

什么是 re-render(重新渲染)?哪些是必要的 re-render?哪些是非必要的 re-render?如果你对这些问题还不是很明白,那么可以在这篇文章中找到答案。本文是一篇比较详尽的 React re-render 指南,会着重介绍和解答以下问题:什么是 re-render、哪些是必要或非必要 的 re-render、什么条件能够触发 React 组件 re-render。除此之外,还会介绍一些避免 re-render 的重要开发模式,以及引起不必要 re-render 的反模式。并且针对每种模式和反模式都提供了示例图。

1. 什么是 re-render

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

当页面更新数据的时候,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 可以嵌套使用,其中每个 hooks 都 “属于” 使用它的组件,相同的规则适用于其中任何一个 hooks。

re-render 误区:**props 变化**

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

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

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

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

组件复合避免 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 值。

React.memo: 作为 props 或 children 的组件

React.memo 可用于作为 props 或 children 的元素。仅对父组件进行包装是不起作用的:children 和 props 作为对象类型会在每次父组件渲染时都会变化,进而引起子组件重新渲染。

5. 用 useMemo/useCallback 提高 re-render 性能

反模式:无用的 useMemo/useCallback props

将子组件的 props 包装成 memoize 值,是不能避免该子组件重新渲染的。只要父组件重新渲染,那么子组件就会被重新渲染,与 props 没有关系。

有用的 useMemo/useCallback

如果子组件被 React.memo 包装,那么它的所有非基础数据类型的 props 也要做 memoize 处理。

如果组件使用的 hooks(比如 useEffect、useMemo、useCallback)依赖了非基础数据类型的值,那么要做 memoize 处理。

使用 useMemo 处理昂贵的计算

useMemo 的其中一个使用场景是避免每次重新渲染时进行昂贵的计算。useMemo 也是有其成本的(消耗一点内存并使初始渲染稍微慢一点),因此不应将其用于每次计算。在 React 中,挂载和更新组件在大多数情况下都是最昂贵的计算(除非您实际计算的是基础类型,而您无论如何都不应该在前端这样做)。

因此,useMemo 的典型场景是对 React 元素做 memoize 处理。与组件更新相比,数组的排序或过滤等“纯” JavaScript 操作的成本基本可以忽略不计。

6. 提高列表的重新渲染性能

除了上述的 re-render 规则和模式之外,key 的值会影响列表的渲染性能。仅仅设置 key 值并不会提高列表的性能。为了避免列表元素的重新渲染,还要用 React.memo 包装它们,并且还要遵循一些最佳实践。

key 的值应为字符串,列表中的元素在每次重新渲染时,这个字符串要保持一致。通常使用数据的 id 或 数组的索引作为 key 值。如果列表是静态的,比如元素不会有增加、删除、插入、排序等操作,使用数组的 index 作为 key 是没有问题的。但是在动态列表中使用数组的索引会导致一些问题:

反模式:使用随机数作为 key 值

随机数不要作为列表的 key 值,因为随机数会导致列表元素在每次重新渲染时重新挂载元素:

7. 避免 Context 引起 re-render

将 Provider 的值做 memoize 处理

如果 Context Provider 没有在页面的根节点上,那么祖先节点的变化也会导致它被重新渲染,所以它的值也应该被 memoize。

避免 Context 引起 re-render:对数据和 API 做拆分

如果在 Context 中存在数据和 API 的组合(getter 和 setter),则可以将它们拆分为同一组件下的不同 Providers。这样,仅使用 API 的组件在数据变化时不会重新渲染。

避免 Context 引起 re-render:对数据拆分

如果 Context 管理几个独立的数据块,则可以将它们拆分为同一 Provider 下的较小 Provider。这样,只有变化的数据块的使用者才会重新渲染。

避免 Context 引起 re-render:Context selector

对于那些部分使用了 Context 值的组件,即使使用了 useMemo hooks 也无法避免组件的重新渲染。但是,我们可以使用高阶组件和 React.memo 可以伪造出 Context selector。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8