#说明

借鉴查阅的资料:重绘与回流详解及优化处理方案

一、浏览器解析渲染页面

浏览器解析渲染页面分为一下五个步骤:

  1. 根据HTML解析出DOM树
  2. 根据CSS解析生成CSS规则树
  3. 结合DOM树和CSS规则树,生成渲染树
  4. 根据渲染树计算每一个节点的信息
  5. 根据计算好的信息绘制页面

1、根据HTML解析DOM树

  • 根据HTML的内容,将标签按照结构解析成为DOM树,DOM树解析的过程是一个深度优先遍历。即先构建当前节点的所有子节点,再构建下一个兄弟节点。
  • 在读取HTML文档,构建DOM树的过程中,若遇到script标签,则DOM树的构建会暂停,直至脚本执行完毕。

2、根据CSS解析生成CSS规则树 —》【CSSOM树】

  • 解析CSS规则树时js 执行将暂停,直至CSS规则树就绪。
  • 浏览器在CSS规则树生成之前不会进行渲染。

3、结合DOM树和CSS规则树,生成渲染树

  • DOM树和CSS规则树全部准备好了以后,浏览器才会开始构建渲染树。
  • 精简 CSS 并可以加快CSS规则树的构建,从而加快页面相应速度。

4、根据渲染树计算每一个节点的信息(布局)

  • 布局:通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸
  • 回流:在布局完成后,发现了某个部分发生了变化影响了布局,那就需要倒回去重新渲染。

5、根据计算好的信息绘制页面

  • 绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。
  • 重绘:某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的重绘
  • 回流:某个元素的尺寸发生了变化,则需重新计算渲染树,重新渲染。

二、性能优化之回流重绘

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

例如background-color等不关于页面结构布局的变化就不会产生回流

1、回流

当Render Tree中部分或全部元素的尺寸、结构、或某些属性发送改变时,浏览器重新渲染部分或全部文档的过程称为回流。

换句话说布局或者几何属性需要改变时就会触发回流

会导致回流的操作
  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类
  • 查询某些属性或调用某些方法 eg:clientWidth、clientHeight、clientTop、clientLeft

2、重绘

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、backgrond-color等),浏览器会将新样式赋予给元素并重新描绘它,这个过程称为重绘。

举个栗子

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

3、优化

Ⅰ - 浏览器优化

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

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

  1. offsetTopoffsetLeftoffsetWidthoffsetHeight
  2. scrollTopscrollLeftscrollWidthscrollHeight
  3. clientTopclientLeftclientWidthclientHeight
  4. widthheight
  5. getComputedStyle()
  6. getBoundingClientRect()

所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。

Ⅱ - CSS层面减少回流和重绘

尽可能在DOM树的最末端改变class

回流可以自上而下,或自下而上的回流的信息传递给周围的节点。回流是不可避免的,但可以减少其影响。尽可能在DOM树的里面改变class,可以限制了回流的范围,使其影响尽可能少的节点。例如,你应该避免通过改变对包装元素类去影响子节点的显示。面向对象的CSS始终尝试获得它们影响的类对象(DOM节点或节点),但在这种情况下,它已尽可能的减少了回流的影响,增加性能优势。

  1. <div class="box">
  2. <div class="children1">子节点一</div>
  3. <p>这是文字,不受啥影响的</p>
  4. </div>

就尽可能直接操作children1而不是通过手动修改box去影响children1,当然还得看具体业务需求。

避免设置多层内联样式

我们都知道与DOM交互很慢。我们尝试在一种无形的DOM树片段组进行更改,然后整个改变应用到DOM上时仅导致了一个回流。同样,通过style属性设置样式导致回流。避免设置多级内联样式,因为每个都会造成回流,样式应该合并在一个外部类,这样当该元素的class属性可被操控时仅会产生一个reflow

  1. <div>
  2. <a> <span></span> </a>
  3. </div>
  4. <style>
  5. span {
  6. color: red;
  7. }
  8. div > a > span {
  9. color: red;
  10. }
  11. </style>

对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,

但是对于第二种设置样式的方式来说:

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

动画效果应用到position属性为absolute或fixed的元素上

动画效果应用到position属性为absolute或fixed的元素上,它们不影响其他元素的布局,所它他们只会导致重新绘制,而不是一个完整回流。这样消耗会更低。

牺牲平滑度换取速度

Opera还建议我们牺牲平滑度换取速度,其意思是指您可能想每次1像素移动一个动画,但是如果此动画及随后的回流使用了100%的CPU,动画就会看上去是跳动的,因为浏览器正在与更新回流做斗争。动画元素每次移动3像素可能在非常快的机器上看起来平滑度低了,但它不会导致CPU在较慢的机器和移动设备中抖动。

避免使用table布局

避免使用table布局。可能您需要其它些避免使用table的理由,在布局完全建立之前,table经常需要多个关口,因为table是个和罕见的可以影响在它们之前已经进入的DOM元素的显示的元素。想象一下,因为表格最后一个单元格的内容过宽而导致纵列大小完全改变。这就是为什么所有的浏览器都逐步地不支持table表格的渲染(感谢Bill Scott提供)。然而有另外一个原因为什么表格布局时很糟糕的主意,根据Mozilla,即使一些小的变化将导致表格(table)中的所有其他节点回流。

Ⅲ - JavaScript层面较少回流和重绘

避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

举个栗子

上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:

  1. function initP() {
  2. for (let i = 0; i < paragraphs.length; i++) {
  3. paragraphs[i].style.width = box.offsetWidth + 'px';
  4. }
  5. }

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

  1. const width = box.offsetWidth;
  2. function initP() {
  3. for (let i = 0; i < paragraphs.length; i++) {
  4. paragraphs[i].style.width = width + 'px';
  5. }
  6. }