React性能优化(四):提高页面渲染效率 🚀

1,814 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

大家好,我是疯狂的小波。 在前面2节,我们讲到了如何避免 React 组件非必要的重新渲染,来提高性能。那当我们的组件数据正常更新重新渲染时,怎么提高渲染的效率呢?这一节我们我们将会介绍提高页面更新时渲染效率的几个方法。

React的更新渲染机制

在这之前,我们需要先了解下 React 的更新渲染机制,这样才能做到有的放矢,针对性的进行优化。

graph TD
    state数据改变 --> 根据最新的数据生成新节点 --> 新节点与上次渲染时VDOM树对比-diff --> 找出差异内容并打标 --> 生成最新的VDOM树  --> 打标差异内容渲染到真实DOM

如上图所示,React 采用的是虚拟DOM (即 VDOMReact中也叫Fiber)。每次state数据发生变化的时候,React 会检测当前最新的节点和上次渲染的Fiber树之前的差异,然后针对差异的地方进行打标,返回最新的Fiber树,最后将所有打标的差异内容渲染到 真实DOM。这就是整个 更新渲染 的过程。

为了获得更优秀的性能,减少更新花费的时间、提高效率。首先映入脑海的便是 减少 diff 的过程,以及减少前后 VDOM 树的差异性,提高 diff 的效率,那么在保证应该更新的节点能够得到更新的前提下,怎么来实现呢?

提高更新性能的2个方向

一、减少 diff 的过程

其实我们前面的一、二节,优化方向就是减少 diff 的过程。组件数据没有更新的时候不重新渲染,这一部分的组件就不会被 diff 计算,直接复用之前的DOM,减少了 diff 的计算过程、这部分组件的重新渲染过程,自然就提高了更新的效率。

二、减少前后 VDOM 树的差异性,提高 diff 的效率

先来看看,Reactdiff算法 的3个基本原理

  • 永远只比较同层节点,不会跨层级比较节点。同层节点间默认按顺序比较。
  • 不同的两个节点产生不同的树。节点类型不同的时候,把原来的节点以及它的后代全部干掉,替换成新的。
  • 通过 key 值指定哪些元素是相同的。

通过上面的原理,我们可以看看通过哪些方式可以优化 diif 的性能:

1. DOM层级不要变

{
    flag ?
        <div className="component">
            <!--子节点内容-->
        </div>
        :
        <div className="outer-box">
            <div className="component">
                <!--子节点内容-->
            </div>
        </div>
}

如上,在 flag 属性设置为 false 时,在 component 元素外又包裹了一层节点。这样就会导致重新渲染时 component 及其所有子元素都不能被复用,都会进行重新渲染。因为 diff 只会对比同层级的节点。

2. 相同内容节点类型不要变

{
    flag ?
        <div className="component">
            <!--子节点内容-->
        </div>
        :
        <span className="component">
            <!--子节点内容-->
        </span>
}

如上,在 flag 属性设置为 false 时,原本的 div 元素变更为了 span,这时,哪怕其他所有内容都没有变化,.component 及其所有子元素都会被销毁然后重新渲染。

3. 循环元素添加 key

{
    list.map(item => (
        <div className="item" key={item.id}></div>
    ))
}

同层元素如果没有添加 key 值,diff默认是按照节点顺序比较的,此时如果在头部或中间插入一个新元素,那在插入元素的后续所有元素都不会被直接复用,而是都会进行一次diff计算,再进行更新。而添加了 key 值后,会根据 key 找到之前的元素进行复用,只会新增这个插入的元素,其他的元素只是变换了位置。

这也是为什么通常我们 list 列表都建议加上 key 值的原因。

注意:key 只需要保持当前循环内唯一,不需要全局唯一;并且应该具备稳定性,相同元素的 key 应该始终是相同的,所以将索引或随机数作为 key 值,是没有效果的。

4. 保持结构的稳定性,避免兄弟节点错位而严重影响性能

在非循环的结构中,往往会因为条件渲染导致渲染前后节点不一致,这种情况通常是不会添加 key 的。此时,如果兄弟节点更新前后位置错位,那么后续全部的比较都会错位导致无法复用(因为同级节点默认是按顺序进行 diff 的),对性能大打折扣。

{flag && <div className="loading"></div>}

<div className="component">
    <!--子节点内容-->
</div>
<div className="component2">
    <!--子节点内容-->
</div>

如上,在 flag 值进行切换时,当前节点头部会新增或销毁 loading 节点。此时新旧DOM对比,loading 后面的所有同级节点就算没有任何变化,照样没有重用之前的DOM。如果在 loading 之后还有一万个兄弟节点,那么也全部都无法直接复用,包括如果有子节点内容也是全部重新渲染。所以这种情况下,是非常影响性能的。

那怎么解决这一问题呢?有以下几种方式:

  • 通过样式来控制显示隐藏、而不是使用条件渲染; 比如通过条件切换class名,或style属性,这样只会更新该节点的属性,不会对其他的节点产生影响
{ <div className={`loading ${flag ? '' : 'hidden'}`}></div> }
  • 在隐藏时给一个空节点来保证对比前后能找到同一位置。不影响后续兄弟节点的比较;
{flag ? <div className="loading"></div> : <div></div>}
  • 或者使用一个标签将条件渲染的元素包裹起来,和上一条的原理是一样的。
<div>
    {flag && <div className="loading"></div>}
</div>

5. Taro中的BUG:删除楼层节点会导致所有的兄弟节点数据更新

Taro 中使用 React 进行开发时,会有一个新的性能问题,这是由于 Taro 框架导致的。

在上一条中提到,兄弟节点的错位会导致后续节点的重新更新,而在这之前的节点不会受到影响。基于这条规则,我们上例中的 laoding 组件如果放在同级节点的最后,那么就不会对其他节点产生影响。而这是在React中的规则,在React中最后一个节点的条件渲染通常我们也不需要特殊处理。

而在Taro中则存在一个BUG,不管节点的位置在哪,节点被删除时,setData 的数据是同级所有节点的信息。

<div>
    <div className="component">
        <!--子节点内容-->
    </div>
    <div className="component2">
        <!--子节点内容-->
    </div>
    
    {flag && <div className="loading"></div>}
</div>

如上代码,在React中是没有问题的,条件渲染loading不会对其他组件产生影响。但是在Taro中,falg 切换为 falseloading 被移除时,setData 的数据会设置同级所有节点数据。

所以在待删除节点的兄弟节点的 DOM 结构比较复杂时,如同级节点多或层级多,删除操作的副作用会导致 setData 数据量较大,从而影响性能。

这时我们也可以参考上面第 4 条(保持结构的稳定性)中的几个方案解决这一问题。

关于Taro中的这个BUG可以参考:删除楼层节点要谨慎处理

官网说是在 Taro V3.1版本中会修复这一问题,但是实测在 V3.3.16 中还是存在这一问题

总结

通过了解React的更新渲染机制,我们发现可以通过2个方面来提高更新效率、提升性能。

1、减少 diff 的过程:也就是我们前2节介绍的内容;

2、减少前后 VDOM 树的差异性,提高 diff 的效率,可以通过以下几个方式来实现:

  • DOM层级不要变
  • 相同内容节点类型不要变
  • 循环元素添加 key
  • 保持结构的稳定性,避免兄弟节点错位而严重影响性能
  • Taro中的BUG:删除楼层节点会导致所有的兄弟节点数据更新

通过上述的几种方式,我们就可以有效提高 React 中的更新性能。

推荐阅读