React性能优化:从再渲染触发条件到再渲染优化
Author:zhoulujun Date:
渲染(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:带有属性(PROPS)的组件
所有不是值类型的属性都必须被记忆(memo),以便 React.memo 工作
REACT.MEMO:组件作为PROP或CHILDREN
React.memo 必须被应用于作为子元素/属性传递的元素。对父组件进行memo化处理是行不通的:子元素和属性都是对象,所以它们会随着每次重新渲染而改变。
推荐阅读:React Element、子组件、父组件和重新渲染的奥秘
使用 USEMEMO/USECALLBACK 提高重新渲染性能
反模式:PROPS 上不必要的 USEMEMO/USECALLBACK
父组件缓存prop不会阻止子组件的重新渲染。如果父组件重新渲染,它将触发子组件的重新渲染,而不管其prop如何。
必要的 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