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