浏览器重绘重排及优化整理

  • start date: 2018.01.02

1.概念

1.1 浏览器渲染原理

先祭图
webkitflow.png

从中可以看出浏览器的渲染过程:
浏览器渲染过程如下:

  • 1.解析树:解析HTML,生成DOM树,解析CSS,生成CSSOM树;
  • 2.结合树:将DOM树和CSSOM树结合,生成渲染树(Render Tree);
  • 3.Layout(回流): 根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小);
  • 4.Painting(重绘): 根据渲染树以及回流得到的几何信息,得到节点的绝对像素;
  • 5.Display(展示): 将像素发送给GPU,展示在页面上。

p-1.png

渲染过程看起来很简单:
bg.png

为了构建渲染树,浏览器主要完成了以下工作:

  • 从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
14
documentElement (html)
head
title
body
p
[text node]

div
[text node]

div
img

...

而渲染树就不同了,她只有哪些需要绘制出来的元素,所以不可见节点以及被隐藏的div都不会出现在渲染树中。

1
2
3
4
5
6
7
8
9
10
11
12
root (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
7
width	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
6
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()
getBoundingClientRect()
currentStyle in IE

因为浏览器必须给你最新的值,所以当你进行这些取值操作的时候会立刻触发一次页面的绘制。

如下代码,请问新版Chrome浏览器执行了几次重排呢?

1
2
3
4
5
for (let i = 0; i < 1000; i++) {
let dom = document.createElement('p');
dom.innerText = 1;
document.appendChild(dom);
}

其实以上代码在新版浏览器里只会触发一次重排。很神奇吧,这是新版浏览器所做的优化,类似于React的调度更新机制,阶段更新,这才是真的高效先进。

3.2 CSS及动画优化

  • 直接改变元素的className,避免设置大量的style属性,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow;减少通过js代码修改元素样式。
  • 少用css表达式
  • 使用cssText,如

    1
    2
    const 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集合(类数组)来遍历,因为集合遍历比真数组遍历耗费更高。
  • 用事件委托来减少事件处理器的数量。

相关链接