重绘和回流以及如何优化
1、浏览器渲染机制
浏览器采用流式布局模型(Flow Based Layout)
浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree)。
有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。
为了构建渲染树,浏览器主要完成了以下工作:
从DOM树的根节点开始遍历每个可见节点。
对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
根据每个可见节点以及其对应的样式,组合生成渲染树。
第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:
一些不会渲染输出的节点,比如script、meta、link等。
一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。
注意:渲染树只包含可见的节点.
2、回流 (refolw)
节点的几何属性或者布局发生改变被称为回流,一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流,所以一个节点的回流会引起页面某个部分甚至整个页面的回流。
3、重绘(repaint)
节点的样式改变且不影响布局的,比如color,visibility等,称为重绘。
** 重绘不一定回流,回流一定重绘。 **
4、 浏览器优化
现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。
主要包括以下属性或方法:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()
getBoundingClientRect
所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。
这里我有思考一个问题,为什么强制清空浏览器UI渲染队列会引起性能问题?
参考这篇文章 《https://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-动画算法/》 说说FPS的东西。
相当一部分的浏览器的显示频率是16.7ms, 就是上图第一行的节奏,表现就是“我和你一步两步三步四步往前走……”。如果我们火力搞猛一点,例如搞个10ms setTimeout,就会是下面一行的模样——每第三个图形都无法绘制(红色箭头指示),表现就是“我和你一步两步 坑 四步往前走……”。
国庆北京高速,最多每16.7s通过一辆车,结果,突然插入一批setTimeout的军车,强行要10s通过。显然,这是超负荷的,要想顺利进行,只能让第三辆车直接消失(正如显示绘制第三帧的丢失)。然,这是不现实的,于是就有了会堵车!
同样的,显示器16.7ms刷新间隔之前发生了其他绘制请求(setTimeout),导致所有第三帧丢失,继而导致动画断续显示(堵车的感觉),这就是过度绘制带来的问题。不仅如此,这种计时器频率的降低也会对电池使用寿命造成负面影响,并会降低其他应用的性能。(这里是原文)
我比较认同下面这个观点
‘setTimeout为10ms,为什么我感觉应该丢失的是第二帧,第一帧10ms后进行绘制,此时浏览器刚开始进行渲染。然后过10ms,此时还没达到浏览器的显示频率是16.7ms,所以第二帧丢失,再过10ms,距离第一次绘制已经过去20ms了大于16.7ms,所以第三帧可以绘制。
上面这点歧义也只是计算上有点问题,但是问题原作者已经给我们解释得很清楚了。
过度绘制带来的问题一是会出现掉帧的情况(丢失某一时间点的动画)造成卡顿,二是大量的UI渲染任务聚集在后面会导致电池寿命及其他应用的性能。
而强制清空浏览器UI渲染队列会引起大量的UI渲染任务聚集在一次EventLoop中(同步中)去完成,自然会导致上面第二点,大量的UI渲染任务聚集在后面会导致电池寿命及其他应用的性能。
5、减少回流和重绘
5.1、批量修改DOM或者样式
最核心的思想就是对于对引起回流和重绘的操作不要一次一次的修改,应该用一个容器装起来,一次性修改
比如修改样式如下:
const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
//可以优化为使用cssText或者class统一添加
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
//或者
const el = document.getElementById('test');
el.className += ' active';
再比如增加DOM节点:
考虑我们要执行一段批量插入节点的代码:
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}
const ul = document.getElementById('list');
appendDataToElement(ul, data);
如上面这种写法会导致多次回流
优化方式可以用document.createDocumentFragment
const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
前面有说到渲染树回流、重绘都是针对可见元素,那么就有以下优化方案
原理就是利用display:none隐藏元素,进行各种增删改元素的操作,操作完再使其可见,对display:none隐藏元素进行操作是不会引起回流的。下面代码只会在ul显示的时候(display:block的时候)进行一次回流。
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
5.2、避免触发同步UI渲染
在前面浏览器优化那一章中,提到那些关于获取节点(元素)布局信息的属性不能过分使用,会引起浏览器的强制回流或重绘,也就是在同步中进行回流重绘,所以我们也需要减少对这类属性的使用。
例子如下
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:
const width = box.offsetWidth;
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
5.3、对于复杂动画效果,使用绝对定位让其脱离文档流
对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流
可以看这个例子,可以打开调试面板查看FPS的变化
例子中在未优化前,元素未绝对定位,依靠margin的变化进行动画,引起大量的回流,而当使用了绝对定位,明显感觉FPS稳定在每秒60次,也就是16.6ms一次。
5.4、css3硬件加速(GPU加速)
比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!
划重点:
- 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。
- 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能
如何使用
常见的触发硬件加速的css属性:
transform
opacity
filters
Will-change
效果
我们可以先看个例子。我通过使用chrome的Performance捕获了动画一段时间里的回流重绘情况,实际结果如下图:
从图中我们可以看出,在动画进行的时候,没有发生任何的回流重绘。如果感兴趣你也可以自己做下实验。
重点
使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘
对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
css3硬件加速的坑
当然,任何美好的东西都是会有对应的代价的,过犹不及。css3硬件加速还是有坑的:
如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。
参考文章链接:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/24
https://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-动画算法/