【性能优化】介绍下重绘和回流(Repaint & Reflow),以及如何进行优化

前言

回流(Reflow)重绘(Repaint) 可以说是每一个web开发者都经常听到的两个词语,我也不例外,可是我之前一直不是很清楚这两步具体做了什么事情。而且很尴尬的是每每提到性能优化的时候,我们可以说出 减少回流及其重绘 可以提高页面性能,当然但是一深入问到有什么方式呢?可能就说不出具体体现了,所以整理一下有关这方面的知识 ↓

一、从浏览器的渲染的过程出发

在这里插入图片描述
从上面这个图上,我们可以看到,浏览器渲染过程如下:

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  3. Layout(回流): 根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  4. Painting(重绘): 根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  5. Display(展示): 将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层,这里我们不展开详细说明)

渲染过程看起来很简单,让我们来具体了解下每一步具体做了什么。

二、生成渲染树

在这里插入图片描述
为了构建渲染树,浏览器主要完成了以下工作:

  1. 从DOM树的根节点开始遍历每个可见节点。
  2. 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
  3. 根据每个可见节点以及其对应的样式,组合生成渲染树。

第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:

  • 一些不会渲染输出的节点,比如<script><meta><link>等。
  • 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。

从上面的例子来讲,我们可以看到span标签的样式有一个display:none,因此,它最终并没有在渲染树上。

注意:渲染树只包含可见的节点

回流(Reflow)

前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。

为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历,我们可以以下面这个实例来表示:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值。(如下图)

重绘(Repaint )

最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

既然知道了浏览器的渲染过程后,我们就来探讨下,何时会发生回流重绘。

三、何时发生回流重绘

我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点

注意:回流一定会触发重绘,而重绘不一定会回流

四、浏览器的渲染机制、优化机制及其处理动画流程

浏览器渲染机制
  • 浏览器采用流式布局模型(Flow Based Layout)
  • 浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree)
  • 有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上
  • 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一
浏览器优化机制

现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值

主要包括以下属性或方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • width、height
  • getComputedStyle()、getBoundingClientRect()

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来

浏览器处理动画流程

在这里插入图片描述

  • 浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree)
  • 分割图层:浏览器根据z-index,和脱离了原来dom层的dom解构进行分层
  • 计算样式:分解图层完毕后,将所有的图层批量进行样式计算。这里有些属性是CPU去计算的,有些属性是GPU去计算的
  • reflow -> relayout -> paint set up -> repaint:这一系列过程其实是页面从回流到重绘发生的步骤,这也是为什么回流必然引起重绘,而重绘却不一定要回流的原因
  • GPU:重绘后的“画布”交给GPU去处理
  • 组合布局:浏览器组合布局,然后页面就出现了

五、如何减少回流和重绘

CSS

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 避免使用table布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 尽可能在DOM树的最末端改变class,回流是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class,可以限制了回流的范围,使其影响尽可能少的节点
  • 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多
<div>
  <a> <span></span> </a>
</div>
<style>
  span {
    color: red;
  }
  div > a > span {
    color: red;
  }
</style>

对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平

  • 将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame
  • 避免使用CSS表达式,可能会引发回流
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层
  • CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能

我们可以先看个 demo例子 。我通过使用chrome的Performance捕获了动画一段时间里的回流重绘情况,实际结果如下图
在这里插入图片描述
从图中我们可以看出,在动画进行的时候,有Layout(回流),既然有回流当然会有painting(重绘)。但是按理论来说使用是没有回流及重绘的,至于这点我查阅了一下其他资料也并没有相关于这方面的说明,如果有我会及时更新上来,亦或许可能是我测试有问题的原因吧,暂不纠结我们需要知道css3硬件加速是可以提升页面性能的 ~

重点

  • 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘
  • 对于动画的其它属性,比如background-color这些,还是会引起重绘的,但是不会引起回流,但在性能面前它还是可以提升的

css3硬件加速的缺点

当然,css3硬件加速还是有坑的:

  • 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
  • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

JavaScript

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性

代码展示:

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';

有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。当然,大部分现代浏览器都对其做了优化,因此,只会触发一次重排。但是如果在旧版的浏览器或者在上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次重排,因此我们可以合并所有的改变然后依次处理,比如我们应该改成 ↓

const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中

代码展示:

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
    	li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}

const ul = document.getElementById('list');
appendDataToElement(ul, data);

使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档,所以应该改成 ↓

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来

代码展示:

function initBox() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
    }
}

这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为 ↓

const width = box.offsetWidth;
function initBox() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流

在这对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流,代码可以看页面 → demo例子

打开这个例子后,我们可以打开控制台,控制台上会输出当前的帧数(虽然不准)。但是我们发现帧数一直都没到60。

我们还可以参考 Performance性能火焰图,可以看出当我们动画不是决定定位的时候,从图中可以看到Rendering(渲染计算,包括回流)和Painting(重绘)在录制的性能阶段一直处于高峰,从环状图也可以看出,这个回流(Layout,可以放大每一个Rendering就可以看到)一直在进行计算着
在这里插入图片描述
当我们点击按钮后,就是将动画脱离文档流后,控制台的fs帧数就可以稳定60啦,我们再去抓取动画的Performance ,此时我们可以看到 Rendering和Painting 所占的比例很少了,可见动画设置为绝对定位脱离文档流,可大大优化我们页面的性能!
在这里插入图片描述

六、整理

最后整理一下css中哪些样式属性会导致回流和重绘,及其什么时候开启GPU加速

触发回流

1)盒子模型相关属性:

  • width * height
  • offset * client * scroll
  • padding * margin
  • border * border-width
  • min-height(width) * max-height(width)
  • display

2)定位和浮动

  • top * bottom * left * right
  • position
  • float
  • clear

3)改变节点内部文字结构

  • text-align * line-height * vertical-align
  • overflow * overflow-y * overflow-x
  • font-family * font-size * font-weight
  • white-space
触发重绘
  • border-style * border-radius
  • visibility * text-decoration
  • color * background * background-image * background-position * background-repeat * background-size
  • outline-color * outline * outline-style * outline-width
  • box-shadow
触发GPU加速

概念:

创建了新的layer,属性改变直接由GPU处理,加快处理速度。使得有一些属性的改变可以略过relayout(回流计算),减少浏览器在动画运行时所需要做的工作

缺点:GPU使用会增加内存消耗,同时也会影响字体的抗锯齿效果,导致文本在动画期间会显得有些模糊

以 chrome浏览器为例,符合以下情况,则会创建一个独立的layer:

1)transform(3d转换)

2) video标签

3)混合插件(如 Flash)

4) isolation == isolate

5)opacity < 1

6)filter != normal

7)z-index != auto || 0 + 父元素display: flex|inline-flex

8)mix-blend-mode != normal

9)position == fixed || absolute

10)-webkit-overflow-scrolling == touch

11)will-change:指定可以形成新layer的元素

七、结语

本文主要讲了浏览器的渲染过程、浏览器的优化机制以及如何减少甚至避免回流和重绘,最后还需要感谢这几位大神对这部分内容的无私奉献 ↓

参考文章:
你真的了解回流和重绘吗
渲染树构建、布局及绘制
reflow回流 repaint重绘 硬件加速

发布了134 篇原创文章 · 获赞 80 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Umbrella_Um/article/details/100744981