• home > webfront > ECMAS > react >

    React性能优化:从再渲染触发条件到再渲染优化

    Author:zhoulujun Date:

    在谈论 React 性能时,需要关注两个主要阶段:初始渲染、重新渲染。重新渲染主要分为两种情况:组件自身状态的变化与父级组件引起的重新渲染,重新渲染何时需要优化呢?

    渲染(render)

    在谈论 React 性能时,我们需要关注两个主要阶段:

    • 初始渲染 - 当组件首次出现在屏幕上时发生

    • 重新渲染 - 已经在屏幕上的组件的第二次和任何连续渲染

    注意这里的渲染(render)其实是 react 中的协调(reconciliation) 过程,对比计算虚拟 DOM 树的差异,等到提交(commit)阶段更新到视图。

    当组件的状态(state)或属性(props)发生变化时,React 会创建一个新的虚拟 DOM 树,并与当前的虚拟 DOM 树进行比较。这个过程称为 "Reconciliation"(协调)。通过比较,React 能够找出实际 DOM 需要进行的最小更新,然后批量执行这些更新。

    当组件的状态(state)或属性(props)发生变化时,React 会触发组件的重渲染。重渲染意味着组件将再次执行 render 方法,生成新的虚拟 DOM 树,并可能导致实际 DOM 的更新

    重新渲染

    什么情况下会触发组件的重新渲染,主要分为两种情况:

    • 组件自身状态的变化(useState、useReducer、useContext)

    • 父级组件引起的重新渲染(props变化也属于这种情况)

    细分的话:

    • 状态(State)改变:当组件内部的状态(this.state 或 useState)发生变化时,React 会自动重新渲染该组件——状态变化是所有重新渲染的“根”源

    • 属性(Props)改变:如果组件接收到新的属性(this.props 或 useEffect 依赖项),React 也会重新渲染该组件。

    • 上下文(Context)改变:如果组件订阅了 React 上下文(React.Context 或 useContext),那么当上下文值发生变化时,所有订阅该上下文的组件都会重新渲染。

    • 父组件重渲染:即使子组件的 props 没有直接变化,如果父组件因为状态或属性的变化而重渲染,那么子组件通常也会重新渲染。这是因为 React 默认情况下会在每次父组件渲染时重新创建子组件的 props,从而导致当一个组件重新渲染时,它也会重新渲染它的所有子组。

    • 钩子(HOOK)变化:钩子内发生的一切都“属于”使用它的组件。关于上下文和状态更改的相同规则在这里适用:

      • 钩子内的状态更改将触发“宿主”组件的不可预防的重新渲染

      • 如果钩子使用了 Context 并且 Context 的值发生了变化,它将触发“宿主”组件的不可预防的重新渲染

    • 手动强制更新(forceUpdate):在类组件中,可以调用 this.forceUpdate() 方法来强制组件重新渲染

    这里会有一个问题就是,一些情况下组件的重新渲染是没必要的,比如子组件并没有使用父组件的状态作为 props 或者 props 并没有更新,但父组件的重新渲染还是会导致子组件的重新渲染

    当父组件渲染时,React会检查是否需要重新渲染其子组件。这并不意味着每次父组件渲染时,其子组件都会无条件地重新渲染。React使用了高效的差异算法(如React Fiber)来最小化不必要的渲染。

    如果子组件的props或state没有变化(对于函数组件,只考虑props;对于类组件,同时考虑props和state),React会尽量避免重新渲染这个子组件。这是通过React的“shouldComponentUpdate”生命周期方法(在类组件中)或React.memo(在函数组件中)来实现的。

    对于函数组件,可以使用React.memo来包裹组件,这样只有当组件的props发生变化时,组件才会重新渲染。

    对于类组件,可以重写shouldComponentUpdate生命周期方法,通过比较新旧props和state来决定是否应该重新渲染组件。

    reconciliation 是从 root 开始,但会跳过所有父节点,到 state 发生变化的组件开始往下重新渲染。

    什么是必要和不必要的重新渲染?

    • 必要的重新渲染 - 当组件数据源发生改变,或组件直接使用了新的数据。例如,如果用户在输入框中数入新内容,则管理其状态的组件需要在每次敲击键盘时更新自身,即重新渲染。

    • 不必要的重新渲染 - 由于错误或低效的应用程序架构,通过不同的重新渲染机制通过应用程序传播的组件的重新渲染。例如,如果用户在输入框中输入,并且在每次敲击键盘时重新渲染整个页面,则该页面已被不必要地重新渲染。

    不必要的重新渲染本身 不是问题 :React 非常快并且通常能够在用户没有注意到的情况下悄悄处理;其性能开销并不大;在没有明显感知的卡顿情况下,可以不必进行优化。

    相反使用 React 提供的 memoization 相关的api造成的性能消耗可能会大于组件的重新渲染,得不偿失。

    Don’t optimize prematurely!(不要过早的性能优化)

    什么时候需要重新染优化?

    如果出现了性能问题,比如重新渲染触发频繁或者在性能开销大的组件上,新渲染发生得太频繁和/或在非常重的组件上发生,这可能会导致用户体验出现“滞后”,每次交互都会出现明显的延迟,甚至应用程序变得完全没有响应

    则就必须用一些手段去跳过不必要的重新渲染。

    举个例子,如Content组件内部有状态进行了更新,则Content会重新渲染,以及其所有的子组件会重新渲染。但可能 Tree 其实并不需要重新渲染,且这个组件渲染耗时较大,这时就需要想办法跳过 Tree 的 re-render。

    防止重渲染

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

    在另一个组件的渲染函数中创建组件是一种反模式,可能是最大的性能杀手。在每次重新渲染时,React 都会重新创建这个组件(即销毁它并从头开始重新创建它),这将比正常的重新渲染慢得多。最重要的是,这将导致以下错误:

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

    • 每次重新渲染时都会在组件中重置状态

    • 没有依赖项的useEffect 每次重新渲染后都会处罚

    • 重新渲染前这个组件被聚焦,重新渲染后焦点将丢失

    推荐阅读:如何编写高性能的 React 代码:规则、模式、注意事项

    在渲染函数中创建组件是错误的

    使用组合防止重新渲染:

    向下移动状态

    当一个组件非常重,而它的其中一个部分状态只用在渲染树的孤立的特定的地方时,这种模式可能是有益的。一个典型的例子是在一个复杂的组件中通过点击按钮来打开/关闭一个对话框,该组件渲染了页面的很大一部分。

    在这种情况下,控制模态对话框外观的状态、对话框本身以及触发更新的按钮都可以封装在一个更小的组件中。因此,较大的组件不会在这些状态更改时重新渲染。

    推荐阅读:React Element 的奥秘、子组件、父组件和重新渲染,如何编写高性能的 React 代码:规则、模式、注意事项

    CHILDREN作为属性

    这也可以称为“围绕子组件的包裹状态”。这种模式类似于“下移状态”:它将状态变化封装在一个较小的组件中。这里的不同之处在于,状态用于包装渲染树的慢速部分的元素,因此不能那么容易地提取它。一个典型的例子是附加到组件根元素的 onScroll 或 onMouseMove 回调。

    在这种情况下,可以将状态管理和使用该状态的组件提取到一个较小的组件中,并且可以将慢速组件作为子组件传递给它。从较小的组件的角度来看,children只是props,因此它们不会受到状态变化的影响,因此不会重新渲染。

    推荐阅读:React Element、children、parents 和 re-renders 的奥秘

    组件作为属性

    与之前的模式几乎相同,具有相同的行为:它将状态封装在一个较小的组件中,而重组件作为 props 传递给它。道具不受状态变化的影响,因此重组件不会重新渲染。

    当一些重组件独立于状态,但不能作为一个组作为子元素提取时,它可能很有用。

    推荐阅读:React 组件作为属性:正确的方式™️,React 元素的奥秘,孩子,父母和重新渲染

    使用memoization防止重新渲染

    首先阅读 《ReactHook详解:memo/useMemo/useCallback等钩子细讲》,

    使用REACT.MEMO防止重新渲染

    在 React.memo 中包装一个组件将停止在渲染树的某处触发的下游重新渲染链,除非该组件的 props 已更改。

    这在渲染不依赖于重新渲染源(即状态、更改的数据)的重组件时很有用。

    使用REACT.MEMO防止重新渲染

    REACT.MEMO:带有属性(PROPS)的组件

    所有不是值类型的属性都必须被记忆(memo),以便 React.memo 工作

    REACT.MEMO:带有属性(PROPS)的组件

     REACT.MEMO:组件作为PROP或CHILDREN

    React.memo 必须被应用于作为子元素/属性传递的元素。对父组件进行memo化处理是行不通的:子元素和属性都是对象,所以它们会随着每次重新渲染而改变。

    REACT.MEMO:组件作为PROP或CHILDREN

    推荐阅读:React Element、子组件、父组件和重新渲染的奥秘

    使用 USEMEMO/USECALLBACK 提高重新渲染性能

    反模式:PROPS 上不必要的 USEMEMO/USECALLBACK

    父组件缓存prop不会阻止子组件的重新渲染。如果父组件重新渲染,它将触发子组件的重新渲染,而不管其prop如何。

    反模式:PROPS 上不必要的 USEMEMO/USECALLBACK

    必要的 USEMEMO/USECALLBACK

    如果一个子组件被包裹在 React.memo 中,所有不是原始类型的 props 都必须使用useMemo

    如果组件在 useEffect、useMemo、useCallback 等钩子中使用非原始值作为依赖项,则应该对其进行记忆。

    USEMEMO 进行耗时的计算

    useMemo 的用例之一是避免每次重新渲染时进行复杂的计算。

    useMemo 有它的成本(消耗一点内存并使初始渲染稍微慢一些),所以它不应该用于每次计算。在 React 中,在大多数情况下,安装和更新组件将是最耗时的计算(除非您实际上是在计算素数,否则您不应该在前端这样做)。

    因此,useMemo 的典型用例是记忆 React 元素。通常是现有渲染树的一部分或生成的渲染树的结果,例如返回新元素的映射函数。

    与组件更新相比,“纯”javascript 操作(如排序或过滤数组)的成本通常可以忽略不计。

    防止由上下文引起的重新渲染

    如果 Context Provider 不是放在应用程序的最根目录,并且由于其祖先的更改,它可能会重新渲染自身,则应该记住它的值。

    防止上下文重新渲染:拆分数据和 API

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

    推荐阅读:如何使用 Context 编写高性能的 React 应用程序

    防止上下文重新渲染:将数据分成块

    如果 Context 管理一些独立的数据块,它们可以被拆分为同一个提供者下的更小的提供者。这样,只有更改块的消费者才会重新渲染。

    推荐阅读:如何使用 Context 编写高性能的 React 应用程序

    防止上下文重新渲染:上下文选择器

    没有办法阻止使用部分 Context 值的组件重新渲染,即使使用的数据没有更改,即使使用 useMemo 钩子也是如此。

    然而,上下文选择器可以通过使用高阶组件和 React.memo 来伪造。

    推荐阅读:React Hooks 时代的高阶组件





    参考文章:

    REACT重新渲染指南:一切尽在掌握 https://xuanye.github.io/react-re-render-guide/  





    转载本站文章《React性能优化:从再渲染触发条件到再渲染优化》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2024_0819_9230.html