目录
重绘与回流的概念
重绘:由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如outline
, visibility
, color
、background-color
等,重绘的代价,因为浏览器必须验证DOM树上其他节点元素的可见性。
回流:回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。
结论:回流必定会发生重绘,而重绘不一定会引起回流。
哪些因素会导致回流?
- 添加或删除可见的元素
- 元素位置改变
- 元素尺寸改变:边距,填充,边框,宽度和高度
- 页面渲染初始化
- 浏览器窗口尺寸改变:resize 事件发生时
- 获取元素的偏移量属性,例如:
scrollTop、scrollLeft、scrollWidth、offsetTop、offsetLeft、offsetWidth、offsetHeight,浏览器为了保证值的正确性,会回流取最新的值
等等…
哪些元素会导致回流?
- 与盒子模型有关的属性
width
Height
Padding
Margin
Display
Border-width
Border
- 与位置和浮动有关的
width
Height
Padding
Margin
Display
Border-width
Border
- 与文字和行间距等等有关的
width
Height
Padding
Margin
Display
Border-width
Border
浏览器优化
现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。
我们在通过以下属性或方法获取值时,必定会引发强制渲染来刷新队列
主要包括以下属性或方法:
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
width
、height
getComputedStyle()
getBoundingClientRect()
我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。
举个栗子来说明上述问题(DEMO来自网络,结合上面的知识理解了一下):
特别说明:以下栗子目前仅仅是在Chrome 87.0.4280.141(正式版本)(64位)下的验证,还不涉及IE、FireFox、Edge等浏览器
<body>
<div id="app"></div>
<div id="app1"></div>
</body>
<script>
function reFlow(){
let el = document.getElementById('app');
let node = document.createElement('h1');
node.innerHTML = 'hello';
el.appendChild(node);
console.log(el.getBoundingClientRect());
}
window.addEventListener('load', function(){
for(let i = 0; i < 100; i++){
reFlow();
}
}
</script>
基于Performance的分析图:
从上图可以看出来,上述代码引起了频繁的回流(layout,图中的紫色方块),再结合下面两个栗子:
<body>
<div id="app"></div>
<div id="app1"></div>
</body>
<script>
function reFlow(){
let el = document.getElementById('app');
let node = document.createElement('h1');
node.innerHTML = 'hello';
//el.appendChild(node);
console.log(el.getBoundingClientRect());
}
window.addEventListener('load', function(){
for(let i = 0; i < 100; i++){
reFlow();
}
}
</script>
结合分析图可以看到这一次并没有出现频繁的layout
<body>
<div id="app"></div>
<div id="app1"></div>
</body>
<script>
function reFlow(){
let el = document.getElementById('app');
let node = document.createElement('h1');
node.innerHTML = 'hello';
el.appendChild(node);
//console.log(el.getBoundingClientRect());
}
window.addEventListener('load', function(){
for(let i = 0; i < 100; i++){
reFlow();
}
}
</script>
可以看到在这种情况下,出现了上述描述的:
现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队,这个栗子中,单纯的100次appendChild操作导致的重绘被放置在了最后;
而对getBoundingClientRect属性的使用导致了每次都会强制渲染刷新队列,进而导致了每次的重绘;
结论:导致重绘的确实是appendChild操作,但是浏览器本身的优化机制会减少周期内频繁的回流,但是对于getBoundingClientRect属性的使用破坏了这一优化机制
减少重绘与回流
-
CSS
- 使用
visibility
替换display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局)(但是我个人感觉意义不是太大,dispaly: none的大部分场景visibility: hidden替代不了)。 - 避免使用
table
布局,可能很小的一个小改动会造成整个table
的重新布局。 - 尽可能在
DOM
树的最末端改变class
,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点。 - 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
-
<div> <a> <span></span> </a> </div> <style> span { color: red; } div > a > span { color: red; } </style>
- 对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的
span
标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的span
标签,然后找到span
标签上的a
标签,最后再去找到div
标签,然后给符合这种条件的span
标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平。 - 将动画效果应用到
position
属性为absolute
或fixed
的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择requestAnimationFrame
。 - 避免使用
CSS
表达式,可能会引发回流。 - 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如
will-change
、video
、iframe
等标签,浏览器会自动将该节点变为图层。(<video> <iframe> <canvas>这三个标签会独立图层)图层多了对于硬件资源来说也是一种开销: - 对于视频使用video标签
- gif图会频繁触发重绘,但是它在标签中,所以没有独立图层,我们可以将其独立出来图层,这样可以使重绘的范围变小
- CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让
transform
、opacity
、filters
这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color
这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。 - 不要把DOM节点的属性值放在一个循环里当成循环里的变量。
-
JavaScript
- 避免频繁操作样式,最好一次性重写
style
属性,或者将样式列表定义为class
并一次性更改class
属性。我们在利用JavaScript进行样式控制的时候,不妨只是改变被控制者的class,而非在函数内部直接修改具体的样式。因为浏览器在我们用JavaScript修改每一个具体样式的时候,都会进行一次重绘,如果是修改了2.2中的顽固属性,还会进行回流,这给浏览器造成了巨大的负担。 - 避免频繁操作
DOM
,创建一个documentFragment
,在它上面应用所有DOM操作
,最后再把它添加到文档中。JavaScript添加多个元素造成的回流其实更恐怖,插入,删除,样式变化,每一项都能造成一次DOM树的重新渲染。而我们的做法可以是先把这些元素都集合在一个方法之内,再调用出来,就能有效的减少回流了。 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
参考文档:
《重绘与回流详解及优化处理方案》
《Web前端:解决浏览器页面回流(reflow)的几种方法》
《性能优化之回流重绘》
《“发得快”踩坑笔记-为什么过渡动画卡顿了?》