搞懂浏览器渲染原理(重排与重绘)

1,534 阅读7分钟

前言

前端巩固基础,终于写到了浏览器篇,虽然浏览器渲染原理大概答得出来,但还是想根据以前的笔记整理一下。

浏览器渲染过程

我们以 Webkit 渲染引擎为例,讲一下浏览器是怎么渲染一个网页的:

  • 浏览器的渲染过程:
  1. 解析 HTML 构建 DOM 树,并行请求 css/image/js
  2. CSS 文件下载完成后被 CSS 解析器解析成 CSSOM 树
  3. 结合 DOM 和 CSSOM 树,生成一棵渲染树(Render Tree)
  4. 布局(Layout),计算出每个节点在屏幕中的位置
  5. 将布局显示(Painting)在屏幕上

在用户访问页面过程中,还会不断重新渲染页面,重新渲染通常是指第 4 步+第 5 步,或者只有第 5 步。

在关于前端性能优化里面,就有一点:减少重排与重绘,因为重排和重绘次数多的话,可能会影响到网页显示速度,给用户带来不流畅甚至卡顿的效果。

重绘

概念

重绘即是指当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程。

触发重绘的属性

重排(回流)

概念

当 DOM 的变化影响了元素的几何信息(DOM 对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。

当网页重新布局的时候,也会将元素重新绘制,比如改变元素的宽度,这个元素乃至周边的 DOM 都需要重新绘制。 所以,重排一定会触发重绘,而重绘不一定会重排

触发重排的属性

大致可分为:

  • 盒模型相关的属性:width, height, margin, display, border 等
  • 定位属性及浮动相关的属性:position, top, float 等
  • 改变节点内部文字结构:text-align, overflow, font-size, line-height, vertical-align 等
  • 进行获取布局信息的操作:offsetWidth, clientHeight, width, scrollTop, getComputedStyle 等

触发重排时会对周围 DOM 重新排列,影响的范围有两种:

  • 全局范围:从根节点 html 开始对整个渲染树进行重新布局。
<body>
  <div>
    <h1>大标题</h1>
    <p>段落</p>
    <ul>
      <li>1</li>
      <li>2</li>
    </ul>
  </div>
</body>

当 p 节点上发生 reflow 时,h1 和 body 也会重新渲染,甚至影响全局。 局部范围重排

  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局

一个 dom 的宽高之类的几何信息定死,然后在 dom 内部触发重排,就只会重新渲染该 dom 内部的元素,而不会影响到外界。

浏览器的渲染队列

div.style.left = '5px'
div.style.top = '5px'
div.style.width = '5px'
div.style.height = '5px'

按我们上面说的,上面的代码理论上应该触发 4 次重排+重绘,但实际是只触发了一次重排,这得益于浏览器的渲染队列机制:当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。

强制刷新队列

div.style.top = '5px'
console.log(div.offsetTop)
div.style.left = '5px'
console.log(div.offsetLeft)
div.style.width = '5px'
console.log(div.offsetWidth)
div.style.height = '5px'
console.log(div.offsetHeight)

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

因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘。在开发中应该尽量避免一行代码一个重排,即是做好分离读写操作,减少性能损耗。

  • 强制刷新队列的 style 样式请求:
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
width, height
getComputedStyle(), 或者 IE的 currentStyle

如何减少重绘与重排

1. 分离读写操作

div.style.top = '5px'
div.style.left = '5px'
div.style.width = '5px'
div.style.height = '5px'
console.log(div.offsetTop)
console.log(div.offsetLeft)
console.log(div.offsetWidth)
console.log(div.offsetHeight)

像上面的代码,我们对它进行分离操作,从 4 次重排变成 1 次重排。

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

2. 将样式修改合并成一次操作

div.style.left = '5px'
div.style.top = '5px'
div.style.width = '5px'
div.style.height = '5px'

虽然大部分浏览器都做了渲染队列优化,但不排除老版本浏览器效率仍然地下,故我们最好还是做更好的处理,比如可以把样式集中合并一次修改,通过 class 或者 cssText 属性

// bad
let left = 5
let top = 5
el.style.left = left + 'px'
el.style.top = top + 'px'

// good
el.className += ' the classname'
// good
el.style.cssText += '; left: ' + left + 'px; top: ' + top + 'px;'

3. 缓存布局信息

避免强制刷新队列

box.style.width = box.clientWidth + 10 + 'px'
box.style.height = box.clientHeight + 10 + 'px'

按浏览器渲染队列机制,按道理回流一次。但遇到 box.clientWidth,重新渲染,所以全部是两次回流

let a = box.clientWidth
let b = box.clientHeight
box.style.width = box.clientWidth + 10 + 'px'
box.style.height = box.clientHeight + 10 + 'px'

4. 离线改变 DOM

  1. 隐藏要操作的 DOM

在要操作 dom 之前,通过 display 隐藏 dom,然后尽量的进行多次修改操作,当操作完成之后,才将元素的 display 属性为可见,因为不可见的元素不会触发重排和重绘。

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

  2. 复制节点,在副本上工作,然后再替换原来的元素

5. 使用 resize 事件时,做防抖和节流处理

6. 对动画元素使用 absolute / fixed 属性。

position 属性为 absolute 或 fixed 的元素,重排开销比较小,不用考虑它对其他元素的影响

7. CSS3 属性优化动画

比如使用 CSS 的 transform 来实现动画效果,避免了回流跟重绘,直接在非主线程中执行合成动画操作。这样子的效率更高,毕竟这个是在非主线程上合成的,没有占用主线程资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。那为什么能避免呢,主要是因为它创建了一个新图层。

  • 图层创建的条件: Chrome 浏览器满足以下任意情况就会创建图层:
  1. 拥有具有 3D 变换的 CSS 属性
  2. 使用加速视频解码的<video>节点
  3. <canvas>节点
  4. CSS3 动画的节点
  5. 拥有 CSS 加速属性的元素(will-change)

一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能,但也不能生成过多的图层。

  • 利用合成的好处:
  1. 合成层的位图,会交由 GPU 合成,比 CPU 处理要快。GPU 硬件加速(规避了回流)是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。
  2. 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
  3. 对于 transform 和 opacity 效果,不会触发 layout 和 paint

当然也要注意,如果为太多元素使用 css3 硬件加速,会导致内存占用较大,同样也会有性能问题。

/* transform为3D,则可开启GPU加速,提高动画性能 */
div {
  transform: translate3d(10px, 10px, 0);
}

参考文章