浏览器渲染优化:理解回流、重绘与性能提升

128 阅读4分钟

在开发前端页面时,性能优化是我们非常关注的领域。尤其是在复杂的用户界面中,如何避免不必要的性能损失至关重要。在浏览器渲染过程中,有几个概念对性能影响尤为重要,其中包括“回流”和“重绘”。

1. 浏览器渲染的过程

浏览器的渲染流程从接收到HTML文件开始,经过多个步骤,最终展示出我们看到的页面。简要来说,浏览器的渲染流程如下:

  1. 解析数据包:浏览器首先接收并解析网络请求的响应数据包,得到HTML文件和相关的CSS文件。
  2. 构建DOM树:浏览器将HTML文件转化为DOM(文档对象模型)树。每一个HTML标签都会转化为一个节点。
  3. 构建CSSOM树:CSS文件中的样式会被解析并生成CSSOM(CSS对象模型)树。
  4. 合成Render树:浏览器将DOM树与CSSOM树合并,得到Render树。这个树代表了可见的页面结构,只有那些会在页面中展示的元素才会出现在渲染树中。
  5. 计算布局:浏览器开始计算每个元素的布局信息,包括位置、大小等。这个过程也叫“回流”。
  6. 绘制阶段:通过GPU来绘制页面上的像素,最终将页面呈现出来。

2. 回流与重绘的区别

在浏览器渲染过程中,回流和重绘是两大关键操作。

回流(Reflow)

回流是指浏览器重新计算元素的布局,通常发生在以下情况:

  • 页面初次渲染。
  • 浏览器窗口大小发生变化。
  • 新的DOM元素被添加或移除。
  • 元素的几何属性发生改变(如宽高、位置、字体大小等)。

回流是一个非常昂贵的操作,因为它涉及到整个页面的重新布局,可能会影响到页面上的多个元素,因此在频繁发生回流时,页面的性能会大大降低。

重绘(Repaint)

重绘是指浏览器重新绘制元素的样式,但不涉及布局的计算。例如,当我们改变元素的颜色、背景时,就会发生重绘,但不会影响到元素的位置或大小。

那么问题来了,文字的大小是导致回流,还是重绘呢?

三秒钟思考

3.. 2.. 1..

答案是重绘哦。

  • 重绘不一定会导致回流。例如,改变文本颜色只会导致重绘,而不会重新计算元素的布局。

问题又又又来了!

接下来这段代码会导致几次回流呢?

element.style.width = '100px'
element.style.height = '200px' 

这就涉及到我们接下来要讲的浏览器的渲染优化机制。

3. 浏览器的渲染优化机制

在浏览器中,为了避免频繁的回流和重绘,采用了渲染队列机制。当多个导致回流的操作发生时,浏览器不会立即执行,而是将这些操作放入渲染队列,继续往下执行。当渲染队列积累到一定数量或者页面没有新的回流操作时,浏览器才会一次性执行这些操作,减少性能的消耗。

所以上面的代码只进行了一次回流哦!

4. 强制执行渲染队列的属性

有些属性会导致浏览器强制清空渲染队列进行回流,常见的有以下几种:

  • Offset家族:offsetWidth/offsetHeight...
  • Client家族:clientWidth/clientHeight...
  • Scroll家族:scrollWidth/scrollHeight...

这些属性一旦被访问,浏览器就会立刻清空渲染队列,计算出当前的样式信息,从而触发回流操作。

5. 减少回流的优化策略

为了提高页面性能,我们可以采取一些优化策略,减少不必要的回流操作。

  1. 先让元素脱离文档流,再修改几何属性,最后再放回文档流
    当我们需要修改某个元素的几何属性(如宽高、位置等)时,可以将该元素先隐藏(display: none),修改完属性后再将其恢复(display: block)。这样可以避免页面上其它元素的回流。

  2. 使用虚拟文档document.createDocumentFragment()
    可以将修改操作放在内存中的虚拟DOM中,修改完之后一次性更新到真实的DOM中,减少不必要的回流。

  3. 使用克隆DOM(clone)
    当需要对DOM进行频繁操作时,可以通过克隆DOM节点来避免每次操作都会导致回流。

6. 小练笔

假设我们有如下代码片段:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">Hello, World!</div>

  <script>
    let el = document.getElementById('app');  //不导致回流
    el.style.width = (el.offsetWidth + 1) + 'px';   //强制清空渲染队列,但此时队列里没有
                                                    //所以不造成回流
    el.style.width = 1 + 'px';                      //最后一次清空渲染队列,造成一次回流
  </script>
</body>
</html>