【笔记】浏览器重绘重排及优化整理
Jul 1, 2019笔记前端性能浏览器重绘重排及优化整理
- start date: 2018.01.02
1.概念
1.1 浏览器渲染原理
先祭图
从中可以看出浏览器的渲染过程:
浏览器渲染过程如下:
- 1.解析树:解析HTML,生成DOM树,解析CSS,生成CSSOM树;
- 2.结合树:将DOM树和CSSOM树结合,生成渲染树(Render Tree);
- 3.Layout(回流): 根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小);
- 4.Painting(重绘): 根据渲染树以及回流得到的几何信息,得到节点的绝对像素;
- 5.Display(展示): 将像素发送给GPU,展示在页面上。
渲染过程看起来很简单:
为了构建渲染树,浏览器主要完成了以下工作:
- 从DOM树的根节点开始遍历每个可见节点;
- 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们;
- 根据每个可见节点以及其对应的样式,组合生成渲染树。
实际渲染的例子
例如有下面这样一段HTML代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<html>
<head>
<title>Beautiful page</title>
</head>
<body>
<p>
Once upon a time there was
a looong paragraph...
</p>
<div style="display: none">
Secret message
</div>
<div><img src="..." /></div>
...
</body>
</html>
那么DOM树是完全和HTML标签一一对应的,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14documentElement (html)
head
title
body
p
[text node]
div
[text node]
div
img
...
而渲染树就不同了,她只有哪些需要绘制出来的元素,所以不可见节点以及被隐藏的div都不会出现在渲染树中。1
2
3
4
5
6
7
8
9
10
11
12root (RenderView)
body
p
line 1
line 2
line 3
...
div
img
...
什么是不可见节点?就是一些不会渲染输出的节点,比如script、meta、link标签等。
或者是一些通过css进行隐藏的节点。比如display:none。
注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的(因为还占据文档空间)。只有
display:none
的节点才不会显示在渲染树上。
1.2 重绘(repaint/redraw)
当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,其触发条件为改变元素外观属性时,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
1.3 重排(重构/回流/reflow)
重排也叫回流,实际上,reflow的字面意思也是回流,之所以有的叫做重排,也许是因为重排更好理解
当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为重排(reflow)。
每个页面至少需要一次重排,就是在页面第一次加载的时候。
在重排的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。重排必定会引发重绘,但重绘不一定会引发重排。
2.触发条件
2.1 触发重绘
重绘发生在元素的可见的外观被改变,但并没有影响到布局的时候。比如,仅修改DOM元素的字体颜色(只有重绘,因为不需要调整布局)。
常见引起浏览器绘制过程的属性包含:1
color border-style visibility background text-decoration background-image background-position background-repeat outline-color outline outline-style border-radius outline-width box-shadow background-size
2.2 触发重排
任何页面布局和几何属性的改变都会触发重排,比如:
- 页面渲染初始化(无法避免);
- 添加或删除可见的DOM元素;
- 元素位置的改变,或者使用动画;
- 元素尺寸的改变——大小,外边距,边框;
- 浏览器窗口尺寸的变化(resize事件发生时);
- 元素内容的改变,比如文本的改变或图片大小改变而引起的计算值宽度和高度的改变;
- 元素字体大小发生改变
- 激活CSS伪类(如:hover)
- 设置style属性
- 读取某些元素属性或调用某些方法
常见引起重排属性和方法:1
2
3
4
5
6
7width height margin padding
display border position overflow
clientWidth clientHeight clientTop clientLeft
offsetWidth offsetHeight offsetTop offsetLeft
scrollWidth scrollHeight scrollTop scrollLeft
scrollIntoView() scrollTo() getComputedStyle()
getBoundingClientRect() scrollIntoViewIfNeeded()
3.优化
3.1 浏览器的优化机制
因为渲染树的改变导致的重绘或重排操作都可能代价很高,浏览器会对这个改动做很多优化。浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
但是有的时候脚本可能会导致浏览器的批量优化无法进行,可能在清空队列之前就需要重新绘制(重绘/重排)页面,强制队列刷新。比如你通过脚本获取这些样式:1
2
3
4
5
6offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()
getBoundingClientRect()
currentStyle in IE
因为浏览器必须给你最新的值,所以当你进行这些取值操作的时候会立刻触发一次页面的绘制。
如下代码,请问新版Chrome浏览器执行了几次重排呢?
1 | for (let i = 0; i < 1000; i++) { |
其实以上代码在新版浏览器里只会触发一次重排。很神奇吧,这是新版浏览器所做的优化,类似于React的调度更新机制,阶段更新,这才是真的高效先进。
3.2 CSS及动画优化
- 直接改变元素的className,避免设置大量的style属性,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow;减少通过js代码修改元素样式。
- 少用css表达式
使用cssText,如
1
2const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;实现元素的动画,它的position属性,最好是设为absoulte或fixed,这样元素脱离了文档流,它的变化不会影响到其他元素。
- 动画实现的速度的选择。比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,可改变如3个像素为单位移动则会好很多。
- 隐藏在屏幕外或在页面滚动时,尽量停止动画。
- 开启动画的GPU加速,把渲染计算交给GPU。常见的触发硬件加速的css属性:*transform、opacity、filters、Will-change**。
- 尽量不要使用table布局。因为table中某个元素旦触发了reflow,那么整个table的元素都会触发reflow。并且在已使用table的场合,可以设置
table-layout:auto;
或者是table-layout: fixed
这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围 - 避免使用
background-attachment: fixed
,这将导致在用户滑动的时候不断触发paint操作。虽然移动端对该样式属性本身兼容就不好
transform 和 position absolute的定位动画
包括我本人,在实现定位动画的时候很习惯性得使用绝对定位和相对定位,比如拖拽,红包雨等。但其实有更好的方式,那就是通过CSS3变换(transform: translate(...)
)。前者会触发浏览器重绘,后者不会。因为transform动画由GPU控制,支持硬件加速。
主线程和合成线程
现代浏览器通常有主线程和合成线程组成。这两个线程一起工作完成绘制页面的任务:
主线程的任务:
- 运行js;
- 计算HTML元素的CSS样式;
- layout(relayout)
- 将页面元素绘制成一张或多张位图
- 将位图发送给合成线程
合成线程任务:
- 利用GPU将位图绘制到屏幕上;
- 让主线程将可见的或即将可见的位图发给自己;
- 计算哪部分页面是可见的;
- 计算哪部分页面是即将可见的(当你滚动页面的时候);
- 在你滚动式移动部分页面
其中GPU擅长的领域:
- 绘制东西到屏幕上
- 一次次绘制同一张位图到屏幕上
- 绘制同一张位图到不同的位置,旋转角度和缩放比例
GPU不擅长:加载位图到内存中
3.3 JS(DOM操作)优化
核心:合并多次的DOM和样式的修改。
display:none
,先设置元素为display:none
,然后进行页面布局等操作;设置完成后将元素设置为display:block
这样的话就只引发两次重绘和重排。- 不要经常访问上述影响浏览器的flush队列属性或方法;如果一定要访问,可以利用变量缓存。将访问的值存储起来,接下来使用就不会再引发回流。
- 动画函数中不要使用setTimeout()/setInterval(),使用requestAnimationFrame()。
- 使用cloneNode(true or false) 和 replaceChild(),引发一次回流和重绘。
- 如果需要创建多个DOM节点,可以使用文档碎片DocumentFragment创建完后一次性的加入document。
- 用innerHTML代替document.write。
- 用querySelectorAll()替代getElementByXX()。
- 少用HTML集合(类数组)来遍历,因为集合遍历比真数组遍历耗费更高。
- 用事件委托来减少事件处理器的数量。
相关链接
- http://taobaofed.org/blog/2016/04/25/performance-composite/
- https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction?hl=zh-cn
- https://jsonzhang.com/blog/%E6%8F%90%E9%AB%98%E9%A1%B5%E9%9D%A2%E7%9A%84%E6%B8%B2%E6%9F%93%E6%80%A7%E8%83%BD/
- https://juejin.im/entry/590801780ce46300617c89b8
- https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment
- https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/
Author
My name is Micheal Wayne and this is my blog.
I am a front-end software engineer.
Contact: michealwayne@163.com