肉烂在锅里

个人站

我是软件研发中心培训主管,我喜欢看动漫,学习web前端开发。


重绘与回流

CSS性能让JavaScript变慢?

在真实的浏览器中,JavaScript引擎和CSS引擎(或是说UI引擎)是位于连个不同的线程中的,虽在不同进程之中,但是两个引擎是并不能并行执行的,执行Js那么页面的样式渲染就会受到阻塞,渲染样式,那么Js的执行就会是受到阻塞。在浏览器渲染页面的过程中,这两个引擎可能不断地切换执行权。

为什么要这样设计浏览器呢?

原因非常简单,因为js在执行的时候可能会获取一些css属性,如果css属性发生变化的同时js去获取,那获取到的css是无意义的,因为js获取的css属性可能发生了变化。所以只有保证在同一时间内只执行同一种脚本才能保证js获取到的属性永远都是最新的且是准确的。

什么是回流?

当render Tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变的时候需要重新构建。这就称之为回流。

简而言之就是在不能简单修改,会改变整体页面布局的样式变化的时候会触发回流。

因为需要重新对页面的样式进行排版,所以回流的代价更大。我们应该尽可能避免回流。

什么是重绘?

当render Tree中的一些元素需要更新属性。而这些属性只是影响元素的外观,风格,而不会影响布局的,不如background-color。这就叫做重绘。

重绘的代价相比回流更少,因为他的改变往往不会影响页面的排版,通常情况下只是对某个元素作出修改即可,保证修改后的结果不会影响排版。不需要大动干戈。

TIP:回流一定触发重绘,重绘不一定触发回流

避免重绘与回流的方法

会触发回流的CSS属性如下:

注意:translate不会触发回流和重绘!!!opacity不会触发回流和重绘!!!

只触发重绘的CSS属性如下:

看到这些属性后,我们就知道了什么CSS属性的改变会引起更大的变化。基本上只触发重绘的CSS操作都仅限于颜色,装饰和背景方面。

其实想要永远避免回流,这一点是不可能做到的。我们只能尽可能的减小回流的规模,从而来降低回流带来的副作用。

要想知道如何减小回流的规模还要从浏览器新建dom的过程下手,具体分为以下6步:

  1. 获取DOM后分割图层。

在这一过程中,浏览器会参考position,overflow,float,translateZ,z-index等按‘高度’分割图层,同一‘高度’的元素都会放到同一个图层中,这一过程的原理比较类似于PS中的图层,整个DOM树的绘制是按照元素所在位置的高度一层一层进行叠加的。

  1. 对每个图层的节点计算样式结果(Recalulate style – 样式重计算)

  2. 为每个节点生成图形和位置(Layout – 回流和重布局)

  3. 将每个节点绘制填充到图层位图中(Paint Setup和Paint – 重绘)

  4. 图层作为纹理上传至GPU

  5. 符合多个图层到页面上生成最终屏幕图像(Composite Layers – 图层重组)

因此,只要我们将需要频繁的回流的DOM元素单独作为一个独立图层,那么这个DOM元素的重绘和回流的影响只会在这个图层中!因此,浏览器在重计算布局的时候,只需要计算这单独的图层即可。

但是这种方式也存在弊端,因为如果图层过多,浏览器在最后一步Composite Layers – 图层重组的时候就会增加运算量,所以,在我们企图通过独立图层的方式来减少回流带来的性能损耗的时候,也应该考虑一下图层重组带来的性能负担。

Chrome创建图层的条件

上面这些方式都可以让Chrome浏览器为元素新建一个图层。

谷歌浏览器会将这些设置了3D相关和透视相关的元素抽到独立的一个图层上(并不是所有设置了3d和透视的抽离到同一个组层中,是每一个元素自己在一个图层中)。

另外video的播放其实是浏览器对视频的每一帧不断的过程重绘的过程,就是因为浏览器不断的重绘,才能让视频持续的播放。

css的动画也会让相关元素进入到独立的图层之中。

Index-z本身就是上下级的概念,所以也会抽出到独立的图层之中。

但是需要注意的是,像gif这样的动态图片,他们其实就和普通的图片一样,并不会被浏览器抽到一个独立的图层之中,但是他却和video类似,都是不断地对每一帧进行重绘。

实战小例

1.、捕捉重绘和回流

这么细节的东西可以通过Chrome浏览器为我们提供的preformence工具来完成。

打开录屏功能,我们就可以看到页面上发生回流和重绘的时候都产生了什么影响。

以PC端的bilibili为例,观察他的轮播图:

从上图可以看到,js解析和浏览器渲染ui的过程是互斥的,他们相互阻塞并没有并行加载。

浏览器在回流的过程中,即浏览器按图层重计算样式进而重新布局的过程。这个过程对应图中紫色的部分,是耗时最多的部分

在回流之后,进入painting的过程,可以发现painting的过程耗时明显比render更短。

仔细观察会发现,在painting后还有一个极短的composite过程,这个过程只有0.13秒,这就是浏览器在对各个图层进行回流和重绘后合并图层的过程。

可以发现,合并图层的时间相对于回流和重绘几乎可以忽略不计。所以我们在开发中应该尽可能的避免回流,可以适当牺牲图层的计算时间。

更改说明:之前的理解存在问题,当浏览器中图层分割过多的时候,composite的过程就会消耗极其大量的时间,甚至有可能超过了重绘和回流的总时间之和!

发现图层结构

浏览器对图层的分化都是有对应原因的。我们可以通过Chrome提供的Layers工具来查看图层结构和各个图层被分离的原因。

还是以B站为例:

可以发现B站原本的图层分割非常的少,为了方便发现图层合并的时间超越render时间,我们需要为B站的dom元素增加一些特殊属性来让浏览器分出更多的图层:

增加范围更大的选择器,设置transform:translateZ属性,就可以强制对div进行图层分割:

然后再次回到Layers工具中,我们就可以发现图层数量明显增多,电脑开始变卡…

再次通过performance录屏

我们会发现,录制时间明显变短,而且存在大段composite的过程。所以如果但我们企图通过分离图层来减少回流带来的影响,那我们也就不得不考虑一下分离图层所带来的副作用,因此只有通过对回流和合并图层所带来的副作用进行比对,我们才能最终选择优化性能的方案。

实战优化点

用translate替代top的改变

top会触发回流但是translate不会。

用opacity替代visibility

看上去的效果是一样的,但是opacity不会触发重绘,而visibility会触发重绘

不要一条一条的修改DOM样式

预先定义好class,然后修改DOM的className

把DOM离线后修改

比如先把DOM给display:none,然后修改100次,然后再把他显示出来。设置display:none虽然会触发一次回流但是相比修改100次DOM要值得。

不要把DOM节点的属性值放在一个循环里作为变量

DOM节点的属性值,这里最经典的是offsetHeight属性和offsetWidth。因为获取这两个属性实际上一定会触发一个回流的过程,因为他会获取DOM元素的最新的位置,而在我们现代的浏览器中,回流是有队列的缓存机制的,当我们回流的数量较多的时候,比如在100ms内浏览器要回流5次,那么这5次就会在一个对流中,等待一起进行回流。然后浏览器才会重新fresh这个队列。而我们如果在循环中不断的获取offsetHeight和offsetWidth,那么js就会为了获取精确的位置信息,就会打破回流队列机制,强制回流。

如果我们需要获取offsetHeight等dom属性,那么就尽可能的先将获取属性的值,然后存储起来。

尽可能不要使用table布局

即使修改的是table的最后一列或者最后一行,也会触发回流,即使你感觉可能不需要回流,但是他确实回流了这个table。所以把tr、td都用其他的块级元素来代替比较好。

动画实现的速度的选择

动画的duration应该适当,不能过短,因为UI引擎和js引擎是相互遏制的关系,如果动画速度过快,就会导致过于频繁的修改DOM属性,UI引擎就在短时间内会更频繁的去阻塞js,UI的渲染速度是远远小于js的执行速度的。另外UI引擎在进行回流和重绘也是要消耗cpu资源的,频繁的修改也会为cpu的性能造成压力。

对于动画新建图层

这一点就不用多说了,动画本身就是一个需要不断回流或者重绘的东西,单独抽出来作为一个图层是值得的。

启用GPU硬件加速

尽可能使用translate3d进行 位移。Translate3d是会启用GPU加速的。

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦