从浏览器渲染原理,浅谈回流重绘与性能优化

前言

“回流(重排)”和“重绘”基本上算是前端的高频词之一,你能够在各个文章及面试题中见到,咱们在讨论这个词的时候,其实讨论的是浏览器的渲染流程。
因此在讨论“回流重绘”以前,咱们还须要掌握一些知识;在它之中,咱们还须要更深刻一点;在这以后,咱们还要懂得怎么去把理论结合到项目实践中去。html

经过这篇文章,你能够学习到的知识:
一、追本溯源,“回流”和“重绘”这个词是如何引出的,在了解这两个词以前咱们还须要了解什么
二、浏览器的渲染流程,“回流”和“重绘”的原理
三、优化浏览器渲染性能,减小“回流”和“重绘”,动手将这些优化应用到实际开发中前端

浏览器的渲染引擎

浏览器的主要组件有:用户界面、浏览器引擎、渲染引擎、网络、用户界面后端、JavaScript解释器、数据存储。html5

浏览器的主要功能就是向服务器发出请求,在浏览器窗口中展现您选择的网络资源。浏览器在解析HTML文档,将网页内容展现到浏览器上的流程,其实就是渲染引擎完成的。node

渲染流程

咱们在这里讨论Gecko和Webkit这两种渲染引擎,其中Firefox 使用的是 Gecko,这是 Mozilla 公司“自制”的呈现引擎。而 Safari 和 Chrome 浏览器使用的都是 WebKit。git

WebKit 渲染引擎的主流程:
github

Mozilla 的 Gecko渲染引擎的主流程:
web

从图 3 和图 4 能够看出,虽然 WebKit 和 Gecko 使用的术语略有不一样,但总体的渲染流程是基本相同的,这里咱们参照Webkit引擎来概况一下:面试

  1. 解析HTML Source,生成DOM树
  2. 解析CSS,生成CSSOM树
  3. 将DOM树和CSSOM树结合,去除不可见元素,生成渲染树(Render Tree)
  4. Layout(布局):根据生成的渲染树,进行布局(Layout),获得节点的几何信息(位置,大小)
  5. Painting(重绘):根据渲染树以及回流获得的几何信息,将 Render Tree 的每一个像素渲染到屏幕上

渲染树与渲染对象

渲染树(render tree)是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的做用是让您按照正确的顺序绘制内容。后端

生成渲染树

为了构建渲染树,浏览器主要完成了如下工做:

  1. 从DOM树的根节点开始遍历每一个可见节点。
  • 某些节点不会被渲染输出,好比scriptlinkmeta等标签节点
  • 经过css隐藏的节点会被忽略,好比“display: none”。可是注意,visibility: hidden是会被渲染的(渲染成一个空框),由于它仍占据布局空间
  1. 对于每一个可见的节点,找到CSSOM树中对应的规则,并应用它们。
  2. 根据每一个可见节点以及其对应的样式,组合生成渲染树。

渲染对象

Firefox 将渲染树中的元素称为“框架”。WebKit 使用的术语是渲染器(renderer)或渲染对象(render object)。 

每个渲染对象都表明了一个矩形区域,一般对应相关节点的css框,包含宽度、高度和位置等几何信息。框的类型受“display”样式属性影响,根据不一样的 display 属性,使用不一样的渲染对象(如 RenderInlineRenderBlockRenderListItem 等对象)。

WebKits RenderObject 类是全部渲染对象的基类,其定义以下:

class RenderObject{
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  //the DOM node
  RenderStyle* style;  // the computed style
  RenderLayer* containgLayer; //the containing z-index layer}

咱们能够看到,每一个渲染对象都有 layoutpaint方法,分别对应了回流和重绘的方法。

回流

渲染对象在建立完成并添加到渲染树时,是将DOM节点和它对应的样式结合起来,并不包含位置和大小信息。

咱们还须要经过 Layout 布局阶段,来计算它们在设备视口(viewport)内的确切位置和大小,计算这些值的过程称为回流布局重排

HTML 采用基于流的布局模型,从根渲染对象(即<html>)开始,递归遍历部分或全部的框架层次结构,为每个须要计算的渲染对象计算几何信息,大多数状况下只要一次遍历就能计算出几何信息。可是也有例外,好比<table>的计算就须要不止一次的遍历。

全局布局和增量布局

全局布局指触发了整个render tree范围的布局,通常是同步触发的,触发缘由可能包括:

  1. 影响全部渲染元素的全局样式更改,例如字体大小更改。
  2. 屏幕大小调整。

增量布局是指对标记为“dirty”的渲染对象进行布局。它通常是异步执行的,浏览器将增量布局的“reflow 命令”加入队列,而调度程序会触发这些命令的批量执行。

可是请求样式信息(例如offsetHeight,
getBoundingClientRect等)的脚本可同步触发增量布局。 

Note: dirty bit
为避免对全部细小更改都进行总体布局,浏览器采用了一种“dirty bit”的系统。若是某个渲染对象发生了更改,或者将自身及其子代标注为“dirty”,则须要进行布局。

"回流"仍是"重排"?

本质上它们是一样的流程,只是在不一样浏览器引擎下的“说法”有所差别。

  • Gecko 将视觉格式化元素组成的树称为 "Frame tree" 框架树。每一个元素都是一个框架;
    对于元素的放置,将其称为 "Reflow" 回流

  • WebKit 使用的术语是 "Render Tree" 渲染树,它由"Render Objects"组成。对于元素的放置,WebKit 使用的术语是 "Layout" 布局(或Relayout重排

重绘

在计算出节点可见性、它们的计算样式以及几何信息后,咱们还须要将渲染树中的每一个节点,转换成屏幕上的实际像素。这一步一般称为“重绘”。

重绘是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每一个可视部分。在重绘阶段,系统会遍历渲染树,并调用渲染对象的“paint”方法,将渲染对象的内容显示在屏幕上。

和布局同样,重绘也分为全局(绘制整个render tree)和增量两种。

绘制顺序

绘制的顺序其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,所以这样的顺序会影响绘制。块渲染对象的堆栈顺序以下:
一、背景颜色
二、背景图片
三、边框
四、子代
五、轮廓

什么时候触发回流和重绘

触发回流(reflow):

回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就须要回流.

改变这些属性会触发回流:

  • 盒模型相关的属性: widthheightmargindisplayborder
  • 定位属性及浮动相关的属性: top,position,float
  • 改变节点内部文字结构也会触发回流:text-align, overflow, font-size, line-height, vertival-align

以及进行如下流程或操做:

  • 页面一开始渲染的时候
  • 添加或删除可见的DOM元素,进行DOM操做等
  • 内容发生变化,好比文本变化或图片被另外一个不一样尺寸的图片所替代
  • 浏览器的窗口尺寸变化(由于回流是根据视口的大小来计算元素的位置和大小的)
  • css伪类激活
  • 进行获取布局信息的操做,好比offsetWidthoffsetHeightclientWidthclientHeightwidthheightscrollTopscrollHeight,getComputedStyle, getBoundingClientRect

Note: 具体属性及操做能够访问这个网站:What forces layout / reflow

触发重绘:

重绘是一个元素外观的改变所触发的浏览器行为,例如改变visibilityoutlinebackground-color等属性,这些属性只是影响元素的外观,风格,而不会影响布局的。

浏览器会根据元素的新属性从新绘制,使元素呈现新的外观。重绘不会带来从新布局,并不必定伴随回流。

Note: 若是想知道更改任何指定 CSS 属性将触发回流仍是重绘,请查看 CSS 触发器

咱们根据渲染的流程可知,回流必定会触发重绘,而重绘不必定会回流

渲染性能优化

回流和重绘的代价是比较昂贵的,渲染性能优化,就是要尽量减小Layout回流和Paint重绘发生的次数,将回流和重绘的影响范围限制在单独的图层以内

合并屡次布局操做

咱们能够合并屡次对DOM和样式的修改,而后一次处理掉,以此来最小化回流和重绘操做,好比:

// bad
const el = document.getElementById('test');
el.style.margin = '5px';
el.style.width = '100px';
el.style.borderRight = '2px';

例子中,有三个样式属性被修改了,每个都会影响元素的几何结构,引发回流。(固然,大部分现代浏览器都对其作了优化,只会触发一次。可是若是在旧版的浏览器或者在上面代码执行的时候,有其余代码访问了布局信息,那么就会致使三次回流)

咱们合并全部的布局操做,而后统一处理,好比这样:

// good
const el = document.getElementById('test');
el.style.cssText += 'margin: 5px;width: 100px;border-right: 2px; '

减小或避免强制同步布局

上面咱们提到,访问一些属性(就是offsetWidth那一堆属性)会致使浏览器强制清空队列,进行强制同步布局。实际使用中能够尽可能避免,若是不能避免,也应该减小。

好比咱们想批量将一些标签的宽度设为某个box的宽度,咱们可能会写成下面这样:

// bad
   for (let i = 0; i < elment.length; i++) {
        elment[i].style.width = box.offsetWidth + 'px';
    }

这段代码看上去问题不大,可是在每次循环的时候,都会去读取box的offsetWidth,致使浏览器每次都会因强制同步布局而触发回流,形成了很大的性能问题。

相似这这状况,咱们能够把读取到的offsetWidth进行缓存:

// good
const width = box.offsetWidth;
for (let i = 0; i < element.length; i++) {
    element[i].style.width = width + 'px';
}

使用 transformopacity 来实现动画

最佳的性能渲染流程,就是直接避开回流和重绘,只运行Composite合成这一操做。

目前能够有合成器单独处理的属性有两个:
transforms 和 opacity

好比咱们可使用translate代替lefttop
使用opacity代替visibility

简化绘制的复杂度、减少绘制区域

除 transform 或 opacity 属性以外,更改任何属性始终都会触发绘制。

绘制一般是像素管道中开销最大的部分;应尽量避免绘制。

经过层的提高来减小绘制区域

绘制并不是老是绘制到内存中的单个图像。事实上,在必要时浏览器能够绘制到多个图像或合成器层,各个层能够在彼此的上面处理并合成,以建立最终图像。

建立新层的最佳方式是使用 will-change CSS 属性。
此方法在 Chrome、Opera 和 Firefox 上有效,而且经过 transform 的值将建立一个新的合成器层:

.moving-element { 
    will-change: transform;
}

对于不支持 will-change 但受益于层建立的浏览器,例如 Safari 和 Mobile Safari,须要开启GPU加速来强制建立一个新层:

.moving-element { 
    transform: translateZ(0);
}

优化或减小动画编排

减小绘制区域每每是编排您的动画和变换,使其不过多重叠,或设法减小动画编排,避免对页面的某些部分设置动画。

下降绘制的复杂性

一些css属性的绘制比其余绘制的开销更大。例如,绘制任何涉及模糊(例如shadow)的元素所花的时间将比绘制一个border的时间要长。

实际开发中,咱们要肯定能否使用一组开销更小的样式,或者替代方式来实现最终结果。

让复杂的布局“离线”

对于复杂的动画,或者频繁触发回流的元素,咱们
建立一个documentFragmentdiv,在它上面应用全部DOM操做,最后再把它添加到window.document

也能够在一个display:none的元素上进行操做,最终把它显示出来。由于display:none上的DOM操做不会引起回流和重绘。

也可使用绝对定位,让它脱离文档流,从而避免引发父元素以及后续元素的频繁回流。

其余

避免使用table布局

咱们已经在上面说过,<table>的计算须要不止一次的遍历,table是能够影响以前已经进入的DOM元素的显示的元素。即便一些小的变化和会致使table中全部其余节点回流。

参考文献

浏览器的工做原理:新式网络浏览器幕后揭秘

Google Developers:渲染树构建、布局及绘制