0%

浏览器的重排和重绘

网页的生成过程是怎样的?

img

在这个过程中:

  1. HTML 被 HTML 解析器解析成 DOM 树

  2. CSS 则被 CSS 解析器解析成 CSSOM 树

  3. 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree)

  4. 生成布局(flow),即将所有渲染树的所有节点进行平面合成

  5. 将布局绘制(paint)在屏幕上

其中,第 4 步和第 5 步是最耗时的部分,这两步合起来就是我们通常所说的渲染。在网页生成的过程中,至少会渲染一次;并且,在用户操作界面的过程中,还会不断地重新渲染。也就是会不断地发生重排和重绘。

什么是重排?

当渲染树 Render tree 中的一部分(或全部)因为 DOM 的变化影响了元素的几何信息(DOM 对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,并将其安放在界面中的正确位置,这个过程就称为重排(reflow),重排也叫作回流。

什么是重绘?

当渲染树 Render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,而没有影响到布局,例如改变 visibility、outline、背景色等属性,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观,这个过程就称为重绘(repaint)。

重排与重绘的关系是怎样的?

重绘不会引起重排,但重排一定会引起重绘。一个元素的重排通常会带来一系列的反应,甚至触发整个文档的重排和重绘,性能代价是高昂的。

什么情况会触发重排和重绘?

1、触发重排的条件

  • 页面渲染初始化时;(这个无法避免)

  • 浏览器窗口改变尺寸;

  • 元素尺寸改变时;

  • 元素位置改变时;

  • 元素内容改变时;

  • 添加或删除可见的 DOM 元素时。

2、触发重绘的条件

常见的引起重绘的属性:

  • 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

display: none 和 visibility: hidden 有什么区别?

  • 通过 display: none 隐藏一个 DOM 节点,会触发重排和重绘;
  • 通过 visibility: hidden 隐藏一个 DOM 节点,只触发重绘,因为没有几何变化。

浏览器的渲染队列是什么意思?

当我们修改了元素的几何属性,导致浏览器触发重排或重绘时,它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。

以下代码将会触发几次渲染?

1
2
3
4
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

根据前面的描述,这段代码理论上会触发4次重排+重绘,因为每一次都改变了元素的几何属性。但实际上最后只触发了一次重排,这都得益于浏览器的渲染队列机制。

但是如果我们像下面这样:

1
2
3
4
5
6
7
8
div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);

这段代码就会触发4次重排+重绘,因为在 console 中请求了这几个样式信息,无论何时浏览器都会立即执行渲染队列的任务(强制刷新队列),即使该值与操作中修改的值没关联。

强制刷新队列的 style 样式请求:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight

  • scrollTop、scrollLeft、scrollWidth、scrollHeight

  • clientTop、clientLeft、clientWidth、clientHeight

  • getComputedStyle()、 或者 IE 的 currentStyle

我们在开发中应当避免这些能够强制刷新队列的操作。

如何优化重排?

1、读写分离

还是拿上面的强制刷新队列的例子,如果我们改成这样:

1
2
3
4
5
6
7
8
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);

这次只触发了一次重排,因为:在第一个 console 的时候,浏览器把之前上面四个写操作的渲染队列都给清空了;剩下的 console,因为渲染队列本来就是空的,所以并没有触发重排,仅仅拿值而已。

2、样式集中改变

将多次改变样式属性的操作合并成一次操作,减少 DOM 访问

1
2
3
4
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

优化:

1
el.className += " className"; // 直接改变 class

3、离线操作

如果要批量添加 DOM,可以让要操作的元素进行「离线处理」,处理完后一起更新。离线处理的意思是:

  • 隐藏要操作的 DOM

  • 在要操作 DOM 之前,通过 display 属性隐藏 DOM,当操作完成之后,再将元素的 display 属性为可见,因为不可见的元素不会触发重排和重绘。

  • 通过使用 DocumentFragment 创建一个DOM 碎片,在它上面批量操作 DOM,操作完成之后,再添加到文档中,这样只会触发一次重排。

4、position 属性的应用

将需要多次重排的元素,position 属性设为 absolute 或 fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

5、在内存中构建

在内存中多次操作节点,完成后再添加到文档中去。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的 html 片段,再一次性添加到文档中去,而不是直接操作 DOM,循环添加每一行。