React性能优化(四):提高页面渲染效率 🚀
大家好,我是疯狂的小波。
在前面2节,我们讲到了如何避免 React 组件非必要的重新渲染,来提高性能。那当我们的组件数据正常更新重新渲染时,怎么提高渲染的效率呢?这一节我们我们将会介绍提高页面更新时渲染效率的几个方法。
React的更新渲染机制
在这之前,我们需要先了解下 React 的更新渲染机制,这样才能做到有的放矢,针对性的进行优化。
如上图所示,React 采用的是虚拟DOM (即 VDOM ,React中也叫Fiber)。每次state数据发生变化的时候,React 会检测当前最新的节点和上次渲染的Fiber树之前的差异,然后针对差异的地方进行打标,返回最新的Fiber树,最后将所有打标的差异内容渲染到 真实DOM。这就是整个 更新渲染 的过程。
为了获得更优秀的性能,减少更新花费的时间、提高效率。首先映入脑海的便是 减少 diff 的过程,以及减少前后 VDOM 树的差异性,提高 diff 的效率,那么在保证应该更新的节点能够得到更新的前提下,怎么来实现呢?
提高更新性能的2个方向
一、减少 diff 的过程
其实我们前面的一、二节,优化方向就是减少 diff 的过程。组件数据没有更新的时候不重新渲染,这一部分的组件就不会被 diff 计算,直接复用之前的DOM,减少了 diff 的计算过程、这部分组件的重新渲染过程,自然就提高了更新的效率。
二、减少前后 VDOM 树的差异性,提高 diff 的效率
先来看看,React 中 diff算法 的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 切换为 false,loading 被移除时,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 中的更新性能。
推荐阅读
转载自:https://juejin.cn/post/7171631228917923877