# 搞懂浏览器渲染原理（重排与重绘）

## 前言

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

## 浏览器渲染过程

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

![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/32c524d3f8804d1eb02f2a550b69bd94~tplv-k3u1fbpfcp-zoom-1.image)

- 浏览器的渲染过程：

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

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

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

## 重绘

### 概念

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

### 触发重绘的属性

![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f9e68749268c42c19559c32cdc52e344~tplv-k3u1fbpfcp-zoom-1.image)

## 重排(回流)

### 概念

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

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

### 触发重排的属性

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d780c2f281334431938eb1feb04581be~tplv-k3u1fbpfcp-zoom-1.image)

大致可分为：

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

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

- 全局范围：从根节点 html 开始对整个渲染树进行重新布局。

```html
<body>
  <div>
    <h1>大标题</h1>
    <p>段落</p>
    <ul>
      <li>1</li>
      <li>2</li>
    </ul>
  </div>
</body>
```

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

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

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

## 浏览器的渲染队列

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

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

## 强制刷新队列

```javascript
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. 分离读写操作

```javascript
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. 将样式修改合并成一次操作

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

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

```javascript
// 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. 缓存布局信息

避免强制刷新队列

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

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

```javascript
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 属性为可见，因为不可见的元素不会触发重排和重绘。

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

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

### 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 硬件加速，会导致内存占用较大，同样也会有性能问题。

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

## 参考文章

- [深入浅出浏览器渲染原理](https://blog.fundebug.com/2019/01/03/understand-browser-rendering/)

<br>

> 欢迎关注我掘金账号和 Github 技术博客：
>
> - 掘金：https://juejin.im/user/1257497033714477
> - Github：https://github.com/Jacky-Summer
> - 觉得对你有帮助或有启发的话欢迎 star，你的鼓励是我持续创作的动力~
> - 如需在微信公众号平台转载请联系作者授权同意，其它途径转载请在文章开头注明作者和文章出处。
