文章目录
前言
回流(Reflow) 和 重绘(Repaint) 可以说是每一个web开发者都经常听到的两个词语,我也不例外,可是我之前一直不是很清楚这两步具体做了什么事情。而且很尴尬的是每每提到性能优化的时候,我们可以说出 减少回流及其重绘 可以提高页面性能,当然但是一深入问到有什么方式呢?可能就说不出具体体现了,所以整理一下有关这方面的知识 ↓
一、从浏览器的渲染的过程出发
从上面这个图上,我们可以看到,浏览器渲染过程如下:
- 解析HTML,生成DOM树,解析CSS,生成CSSOM树
- 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
- Layout(回流): 根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
- Painting(重绘): 根据渲染树以及回流得到的几何信息,得到节点的绝对像素
- Display(展示): 将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层,这里我们不展开详细说明)
渲染过程看起来很简单,让我们来具体了解下每一步具体做了什么。
二、生成渲染树
为了构建渲染树,浏览器主要完成了以下工作:
- 从DOM树的根节点开始遍历每个可见节点。
- 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
- 根据每个可见节点以及其对应的样式,组合生成渲染树。
第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:
- 一些不会渲染输出的节点,比如
<script>
、<meta>
、<link>
等。 - 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。
从上面的例子来讲,我们可以看到span标签的样式有一个display:none,因此,它最终并没有在渲染树上。
注意:渲染树只包含可见的节点
回流(Reflow)
前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。
为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历,我们可以以下面这个实例来表示:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critial Path: Hello world!</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world!</div>
</div>
</body>
</html>
我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值。(如下图)
重绘(Repaint )
最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。
既然知道了浏览器的渲染过程后,我们就来探讨下,何时会发生回流重绘。
三、何时发生回流重绘
我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:
- 添加或删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
- 页面一开始渲染的时候(这肯定避免不了)
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点
注意:回流一定会触发重绘,而重绘不一定会回流
四、浏览器的渲染机制、优化机制及其处理动画流程
浏览器渲染机制
- 浏览器采用流式布局模型(Flow Based Layout)
- 浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree)
- 有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上
- 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一
浏览器优化机制
现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值
主要包括以下属性或方法:
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- width、height
- getComputedStyle()、getBoundingClientRect()
以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来
浏览器处理动画流程
- 浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree)
- 分割图层:浏览器根据z-index,和脱离了原来dom层的dom解构进行分层
- 计算样式:分解图层完毕后,将所有的图层批量进行样式计算。这里有些属性是CPU去计算的,有些属性是GPU去计算的
- reflow -> relayout -> paint set up -> repaint:这一系列过程其实是页面从回流到重绘发生的步骤,这也是为什么回流必然引起重绘,而重绘却不一定要回流的原因
- GPU:重绘后的“画布”交给GPU去处理
- 组合布局:浏览器组合布局,然后页面就出现了
五、如何减少回流和重绘
CSS
- 使用 transform 替代 top
- 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
- 避免使用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,详见探讨 requestAnimationFrame
- 避免使用CSS表达式,可能会引发回流
- 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层
- CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能
我们可以先看个 demo例子 。我通过使用chrome的Performance捕获了动画一段时间里的回流重绘情况,实际结果如下图
从图中我们可以看出,在动画进行的时候,有Layout(回流),既然有回流当然会有painting(重绘)。但是按理论来说使用是没有回流及重绘的,至于这点我查阅了一下其他资料也并没有相关于这方面的说明,如果有我会及时更新上来,亦或许可能是我测试有问题的原因吧,暂不纠结我们需要知道css3硬件加速是可以提升页面性能的 ~
重点
- 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘
- 对于动画的其它属性,比如background-color这些,还是会引起重绘的,但是不会引起回流,但在性能面前它还是可以提升的
css3硬件加速的缺点
当然,css3硬件加速还是有坑的:
- 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
- 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。
JavaScript
- 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性
代码展示:
const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。当然,大部分现代浏览器都对其做了优化,因此,只会触发一次重排。但是如果在旧版的浏览器或者在上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次重排,因此我们可以合并所有的改变然后依次处理,比如我们应该改成 ↓
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
- 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有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 fragment)在当前DOM之外构建一个子树,再把它拷贝回文档,所以应该改成 ↓
const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来
代码展示:
function initBox() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为 ↓
const width = box.offsetWidth;
function initBox() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流
在这对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流,代码可以看页面 → demo例子
打开这个例子后,我们可以打开控制台,控制台上会输出当前的帧数(虽然不准)。但是我们发现帧数一直都没到60。
我们还可以参考 Performance性能火焰图,可以看出当我们动画不是决定定位的时候,从图中可以看到Rendering(渲染计算,包括回流)和Painting(重绘)在录制的性能阶段一直处于高峰,从环状图也可以看出,这个回流(Layout,可以放大每一个Rendering就可以看到)一直在进行计算着
当我们点击按钮后,就是将动画脱离文档流后,控制台的fs帧数就可以稳定60啦,我们再去抓取动画的Performance ,此时我们可以看到 Rendering和Painting 所占的比例很少了,可见动画设置为绝对定位脱离文档流,可大大优化我们页面的性能!
六、整理
最后整理一下css中哪些样式属性会导致回流和重绘,及其什么时候开启GPU加速
触发回流
1)盒子模型相关属性:
- width * height
- offset * client * scroll
- padding * margin
- border * border-width
- min-height(width) * max-height(width)
- display
2)定位和浮动
- top * bottom * left * right
- position
- float
- clear
3)改变节点内部文字结构
- text-align * line-height * vertical-align
- overflow * overflow-y * overflow-x
- font-family * font-size * font-weight
- white-space
触发重绘
- border-style * border-radius
- visibility * text-decoration
- color * background * background-image * background-position * background-repeat * background-size
- outline-color * outline * outline-style * outline-width
- box-shadow
触发GPU加速
概念:
创建了新的layer,属性改变直接由GPU处理,加快处理速度。使得有一些属性的改变可以略过relayout(回流计算),减少浏览器在动画运行时所需要做的工作
缺点:GPU使用会增加内存消耗,同时也会影响字体的抗锯齿效果,导致文本在动画期间会显得有些模糊
以 chrome浏览器为例,符合以下情况,则会创建一个独立的layer:
1)transform(3d转换)
2) video标签
3)混合插件(如 Flash)
4) isolation == isolate
5)opacity < 1
6)filter != normal
7)z-index != auto || 0 + 父元素display: flex|inline-flex
8)mix-blend-mode != normal
9)position == fixed || absolute
10)-webkit-overflow-scrolling == touch
11)will-change:指定可以形成新layer的元素
七、结语
本文主要讲了浏览器的渲染过程、浏览器的优化机制以及如何减少甚至避免回流和重绘,最后还需要感谢这几位大神对这部分内容的无私奉献 ↓