浏览器的重排和重绘

浏览器的重排和重绘

我们都知道,出于性能上的考虑,要尽可能地减少浏览器的重排(reflow)和重绘 (repaint),那这样做的原因是什么呢?具体又应该如何做呢?下面我们一起来看看。

一、网页生成过程

我们先来看看网页的生成过程,如下图:

GUI

(图片来自网络)

在这个过程中:

  1. HTMLHTML 解析器解析成 DOM
  2. CSS 则被 CSS 解析器解析成 CSSOM
  3. 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree
  4. 生成布局(flow),即将所有渲染树的所有节点进行平面合成
  5. 将布局绘制(paint)在屏幕上

其中,第 4 步和第 5 步是最耗时的部分,这两步合起来就是我们通常所说的渲染

在网页生成的过程中,至少会渲染一次;并且,在用户操作界面的过程中,还会不断地重新渲染。也就是会不断地发生重排和重绘。

二、重排和重绘的概念

1、重排

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

每个页面至少需要一次重排,发生在页面第一次加载的时候。

2、重绘

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

3、重排与重绘的关系

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

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

任何关于用来构建渲染树的信息改变,都会导致一次重排或重绘。

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

3、注意

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

四、浏览器的渲染队列

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

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

根据前面的描述,这段代码理论上会触发4次重排+重绘,因为每一次都改变了元素的几何属性。

但实际上最后只触发了一次重排,这都得益于浏览器的渲染队列机制:

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

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

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 样式请求:

  • offsetTopoffsetLeftoffsetWidthoffsetHeight

  • scrollTopscrollLeftscrollWidthscrollHeight

  • clientTopclientLeftclientWidthclientHeight

  • getComputedStyle()、 或者 IEcurrentStyle

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

五、优化重排

1、读写分离

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

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 访问

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

优化:

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

3、离线操作

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

  • 隐藏要操作的 DOM

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

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

4、position 属性的应用

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

5、在内存中构建

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

六、虚拟 DOM

由于重排和重绘的问题,当页面变得复杂的时候,频繁地操作 DOM 会造成很大的性能开销。

JQuery 出现之前,我们都是直接操作 DOM 结构,这种方法复杂度高,兼容性也较差。JQuery 对于操作 DOMAPI 封装是非常强大的,操作 DOM 变得简单了很多,同时也帮我们处理了兼容性问题。但是这依旧不是很好的方案,性能问题依然存在,所以像 ReactVue 等框架,引入了虚拟 DOMVirtual DOM)的概念。

DOM 很慢,但是 JavaScript 很快,虚拟 DOM 使用 JavaScript 对象表示 DOM 节点。DOM 节点包括标签、属性和子节点。

React 在内存中生成维护一个跟真实 DOM 一样的虚拟 DOM 树,在改动完组件后,会再生成一个新得 DOMReact 会把新虚拟 DOM 跟原虚拟 DOM 进行比对,找出两个DOM 不同的地方(diff) ,然后把 diff 放到队列里面,最后批量更新 diff 到真实 DOM 上。所以,最终真实 DOM 就只更新了diff 部分,提高了渲染速度。但是这样也有一些缺点,首次渲染 DOM 时候由于多了一层虚拟 DOM 计算,所以比 html 渲染慢。


相关文章 
1、使用 CircleCI + Github 搭建持续集成环境
2、Web 前端工程化(二) —— CI/CD
3、Web 前端工程化(一) —— 什么是前后端分离?
4、谈谈前端开发中的 MVC、MVP、MVVM 模式
5、微前端
目录