浏览器渲染简述

singsong: 文本是本身看了一些不错资料整理出来的,对该知识点感兴趣的同窗能够查看 参考文章小节。

✏️最新内容请以github上的为准❗️css

为何要写这篇文章?

主要为 CSS 优化工做打一下基础。要编写高性能的网站和应用,除了确保编写的代码尽量高效地运行外,还须要确保页面性能,刷新频率尽可能到达 60FPS。这就须要了解浏览器是如何进行渲染的。而浏览器渲染与 CSS 密切相关,所以只有了解其中工做原理才能让 CSS 更好地工做。html

另外,接下来会出一篇优化实战文章,会涉及 JavaScript 和 CSS 一些优化。其中关于 JavaScript 的优化以前已进行过介绍:常见的 JavaScript 内存泄露。本文是对 CSS 优化进行一个补充。html5

Contents

浏览器

  1. 用户界面(User Interface):包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其余显示的各个部分都属于用户界面。
  2. 浏览器引擎(Browser engine):在用户界面(User Interface)和渲染引擎(Rendering engine)之间传送指令。
  3. 渲染引擎(Rendering engine):负责显示请求的内容。若是请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  4. 网络(Networking):用于网络调用,好比 HTTP 请求。其接口与平台无关,并为全部平台提供底层实现。
  5. JavaScript 解释器(JavaScript Interperter):用于解析和执行 JavaScript 代码。
  6. 用户界面后端(UI Backend):用于绘制基本的窗口小部件,好比组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操做系统的用户界面方法。
  7. 数据存储(Data storage):这是持久层。浏览器须要在硬盘上保存各类数据,例如 Cookies。浏览器还支持诸如 localStorage,IndexedDB,WebSQL 和 FileSystem 之类的存储机制。

本文主要介绍浏览器的渲染,即渲染引擎(Rendering engine)负责的工做: 将请求的 HTML 和 CSS 内容解析并渲染在屏幕上。git

DOM tree

DOM:Document Object Model,文档对象模型。它能够以一种独立于平台和语言的方式,访问和修改一个文档的内容和结构。它定义了一组与平台,语言无关的接口,该接口容许编程语言动态地访问和修改结构化文档。基于 DOM 表示的文档被描述成一个树形结构,使用 DOM 的接口能够对 DOM 树进行操做。github

这里以一个实例展开讲解,以下 HTML 结构包含一些文本和一张图片:web

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

浏览器是如何处理这个 HTML 页面:chrome

  1. 转换(Conversion):浏览器从磁盘或网络读取 HTML 的字节码,并根据文件的指定编码(例如 UTF-8)将其转换成对应的字符。
  2. 令牌化(Tokenizing):浏览器将字符串转换成W3C HTML5 标准规定的各类 token,例如<html><body>,以及其余尖括号内的字符串。每一个 token 都具备必定特殊含义和规则。
  3. 词法分析(Lexing):将 token 转换成定义其属性和规则的“对象”。
  4. DOM tree:HTML 标签订义了不一样标签之间的关系(某些标签包含在其余标签中),所建立的对象连接在树状数据结构中,该数据结构还捕获原始标签中定义的父——子关系:HTML 是 body 的父对象,body 是段落的父对象,依此类推。

整个过程的最终输出是 HTML 页面的 DOM tree,后续浏览器对页面进一步的全部处理都会用到它。浏览器每次处理 HTML 标签时,都会完成以上全部步骤:将字节转换成字符,肯定 token,将 token 转换成节点,而后构建 DOM tree编程

CSSOM tree

CSSOM:CSS Object Model,CSS 对象模型。CSSOM 定义了 JavaScript 访问样式的能力和方式。它是在 DOM 中的一些接口中,加入获取和操做 CSS 属性或接口的 JavaScript 接口,于是 JavaScript 能够动态操做 CSS 样式。DOM 提供了接口让 JavaScript 修改 HTML 文档,CSSOM 提供了接口让 JavaScript 得到和修改 CSS 代码设置的样式信息。canvas

在浏览器构建 DOM 遇到 link 标签时,该标签引用一个外部 CSS 样式表:style.css。因为预见到须要利用该资源来渲染页面,它会当即发出对该资源的请求,并返回如下内容:后端

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

与处理 HTML 时相似,须要将收到的 CSS 规则转换成某种浏览器可以理解和处理的内部表示。所以会重复 HTML 过程,不过是对 CSS 而不是 HTML:

将 CSS 字节转换成字符,接着转换成 token 和节点,最后将它们连接到 CSSOM tree 结构中:

cssom-tree.png

CSSOM tree 能够用于肯定节点对象的计算样式。如 span 标记包含了color:red样式和继承于 body 标记的font-size:16px样式;

RenderObject tree(也称为 Render tree)

  • DOM tree 中,存在不可见与可见节点之分。顾名思义,不可见节点是不须要绘制最终页面中的节点,如metaheadscript等,以及经过 CSS 样式display:none隐藏的节点。相反可见节点是用户可见的,如bodydivspancanvasimg等。对于这些可见节点,浏览器须要将它们的内容绘制到最终的页面中,因此浏览器会为它们创建对应的 RenderObject 对象。一个 RenderObject 对象保存了为绘制 DOM 节点的各类信息。这些 RenderObject 对象与 DOM 对象相似,也构成一棵树,称为RenderObject tree。RenderObject tree 是基于 DOM tree 创建起来的一棵新树,是为了布局计算和渲染等机制而构建的一种新的内部表示。RenderObject tree 节点与 DOM tree 节点不是一一对应关系。由于建立一个 RenderObject 对象须要知足以下规则:
  • DOM tree 的 document 节点
  • DOM tree 中的可见节点,如htmlbodydiv等。而浏览器不会为不可见节点建立 RenderObject 节点。
  • 某些状况下浏览器须要建立匿名的 RenderObject 节点,该节点不对应 DOM 树中的任何节点。

RenderObject 对象构成了 RenderObject tree,每一个 RenderObject 对象保存了为绘制 DOM 节点的计算样式。RenderObject tree 也能够理解成由 CSSOM treeDOM tree 合并成:

Layout(布局)

当浏览器建立 RenderObject 对象以后,每一个对象并不知道本身在设备视口内的位置、大小等信息。浏览器根据盒模型(Box-model)来计算它们的位置、大小等信息的过程称为布局计算(重排)。布局计算是一个递归的过程,这是由于一个节点的大小一般须要先计算它的孩子节点的位置、大小等信息。为了计算节点在页面中的确切大小和位置,浏览器会从 RenderObject tree 的根节点开始进行遍历。

实例:

<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:父 div 将其的显示尺寸设置为 viewport 宽度的 50%,子 div 将其宽度设置为其父项的 50%,即 viewport 宽度的 25%。

RenderLayer tree

浏览器渲染引擎并非直接使用 RenderObject tree 进行绘制,为了方便处理 Positioning(定位),Clipping(裁剪),Overflow-scroll(页內滚动),CSS Transform/Opacity/Animation/Filter,Mask or Reflection,Z-indexing(Z 排序)等,浏览器须要会为一些特定的 RenderObject 生成对应的 RenderLayer,并生成一棵对应的 RenderLayer tree。而这些特定的 RenderObject 跟对应的 RenderLayer 就是直属的关系,若是它们的子节点若是没有对应的 RenderLayer,就从属于父节点的 RenderLayer。最终,每个 RenderObject 都会直接或者间接地从属于一个 RenderLayer。所以 RenderObject 节点与 RenderLayer 节点不是一一对应关系,而是一对多的关系。那须要知足什么条件,渲染引擎才为 RenderObject 创建对应的 RenderLayer:

  • It's the root object for the page
  • It has explicit CSS position properties (relative, absolute or a transform)
  • It is transparent
  • Has overflow, an alpha mask or reflection
  • Has a CSS filter
  • Corresponds to <canvas> element that has a 3D (WebGL) context or an accelerated 2D context
  • Corresponds to a <video> element

翻译:

  • DOM tree 的 Document 节点对应的 RenderObject 节点和 HTML 节点对应的 RenderObject 节点
  • 显式指定 CSS position 属性的 RenderObject 节点
  • 有透明度的 RenderObject 节点
  • 有 overflow,alpha 和 reflection 的样式 RenderObject 节点
  • 有 filter 样式的 RenderObject 节点
  • 使用 Canvas 2D 和 3D(WebGL)技术的 RenderObject 节点
  • video 元素对应的 RenderObject 节点

每一个 RenderLayer 对象能够想象成图像中一个图层,各个图层叠加构成了一个图像。浏览器会遍历 RenderLayer tree,再遍历从属这个 RenderLayer 的 RenderObject,RenderObject 对象存储有绘制信息,并进行绘制。RenderLayer 和 RenderObject 共同决定了最终呈现的网页内容,RenderLayer tree 决定了网页的绘制的层次顺序,而从属于 RenderLayer 的 RenderObject 决定了该 RenderLayer 的内容。

Rendering(渲染方式)

在完成构建 RenderLayer tree 以后,浏览器会使用图形库将其构建的渲染模型绘制出来,该过程分为两个阶段:

  • 绘制:将从属每一个 RenderLayer 图层上的 RenderObject 绘制在其 RenderLayer 上。即绘制(Paint)或者光栅化(Rasterization),将一些绘图指令转换成真正的像素颜色值。

    • 软件绘图:CPU 来完成绘图操做
    • 硬件加速绘图:GPU 来完成绘图操做
  • 合成(compositing):将各个 RenderLayer 图层合并成到一个位图(Bitmap)中。同时还可能包括位移(Translation),缩放(Scale),旋转(Rotation),Alpha 合成等操做。

渲染引擎的渲染,目前有三种网页的渲染方式:

  • 硬件加速合成(Accelerated Compositing):使用 GPU 来完成合成工做。
  • 合成化渲染:使用合成(compositing)技术的渲染称。
  • 软件渲染方式:使用 CPU 来绘制每一个 RenderLayer 图层的内容(RenderObject)到一个位图,即一块 CPU 使用的内存空间。绘制每一层的时候都会使用该位图,区别在于绘制的位置可能不同,绘制顺序按照从后到前。所以软件渲染机制是没有合成阶段的。
  • 硬件加速渲染的合成化渲染方式:使用 GPU 来绘制全部合成层,并使用 GPU 硬件来加速合成。
  • 软件绘图的合成化渲染方式: 某些合成层使用 CPU 来绘图,另一些使用 GPU 来绘制。对于使用 CPU 来绘制的图层,该层的绘制结果会先保存在 CPU 内存中,以后会被传输到 GPU 内存中,而后再使用 GPU 来完成合成工做。

第二种和第三种渲染方式,都是使用了合成化渲染技术,合成工做也都是由 GPU 来作。对于常见的 2D 绘图操做,使用 GPU 来绘图不必定比使用 CPU 绘图在性能上有优点,例如绘制文字、点、线等。缘由是 CPU 的使用缓冲机制有效减小了重复绘制的开销并且不须要考虑与 GPU 并行。另外,GPU 的内存资源相对 CPU 的内存资源来讲比较紧张,并且网页的分层使得 GPU 的内存使用相对比较多。鉴于此,就目前的状况来看,三者都存在是有其合理性的,下面分析一下它们的特色:

  • 软件渲染是目前很常见的技术,也是浏览器最先使用的渲染方式。这一技术比较节省内存,特别是宝贵的 GPU 内存,可是软件渲染只能处理 2D 方面的操做。简单的网页没有复杂绘图或者多媒体方面的需求,软件渲染方式就比较合适来渲染该类型的网页。问题是,一旦赶上了 HTML5 的不少新技术,软件渲染显得无能为力,一是由于能力不足;二是由于性能很差,例如视频、Canvas 2D 等。因此,软件渲染技术被用的愈来愈少,特别是在移动领域。软件渲染同硬件加速渲染另一个很不一样的地方就是对更新区域的处理。当网页中有一个更新小型区域的请求(如动画)时,软件渲染可能只须要计算一个极小的区域,而硬件渲染可能须要从新绘制其中的一层或者多层,而后再合成这些层。硬件渲染的代价可能会大得多。
  • 对于硬件加速的合成化渲染方式来讲,每一个层的绘制和全部层的合成均使用 GPU 硬件来完成,这对须要使用 3D 绘图的操做来讲特别合适。这种方式下,在 RenderLayer 树以后,浏览器还须要创建更多的内部表示,目的是支持硬件加速机制,这显然会消耗更多的内存资源。可是,一方面,硬件加速机制可以支持如今全部的 HTML5 定义的 2D 或者 3D 绘图标准;另外一方面,关于更新区域的讨论,若是须要更新某个层的一个区域,由于软件渲染没有为每一层提供后端存储,于是它须要将和这个区域有重叠部分的全部层次的相关区域一次从后往前从新绘制一遍,而硬件加速渲染只须要从新绘制更新发生的层次,于是在某些状况下,软件渲染的代价又变得更大。固然,这取决于网页的结构和渲染策略。
  • 软件绘图的合成化渲染方式结合了前面两种方式的优势,这时由于不少网页可能既包含基本的 HTML 元素,也包含一些 HTML5 新功能,使用 CPU 绘图方式来绘制某些层,使用 GPU 来绘制其余一些层。缘由固然是前面所述的基于性能和内存方面综合考虑的结果。
浏览器还可使用多线程的渲染架构,将网页内容绘制到后端存储的操做放到另一个独立的线程(绘制线程),而原来线程转为合成线程,绘制线程跟合成线程之间可使用同步,部分同步,彻底异步等做业模式,让浏览器能够在性能与效果之间根据须要进行选择。

GrphicsLayer tree

对于软件渲染而言,到 RenderLayer tree 就结束了,后面不会创建其它额外的树来对应于 RenderLayer tree。可是,对于硬件渲染来讲,在 RenderLayer tree 以后,浏览器渲染引擎为硬件渲染提供了更多的内部结构来支持这个机制。

在硬件加速渲染的合成化渲染和软件绘图的合成化渲染架构下,一个 RenderLayer 对象若是须要后端存储,它会建立一个 RenderLayerBacking 对象,该对象负责 Renderlayer 对象所须要的各类存储。理想状况下,每一个 RenderLayer 均可以建立本身的后端存储,事实上不是全部 RenderLayer 都有本身的 RenderLayerBacking 对象。若是一个 RenderLayer 对象被像样的建立后端存储,那么将该 RenderLayer 称为合成层(Compositing Layer)。

哪些 RenderLayer 对象能够是合成层?若是一个 RenderLayer 对象具备如下的特征之一,那么它就是合成层:

  • Layer has 3D or perspective transform CSS properties
  • Layer is used by < video> element using accelerated video decoding
  • Layer is used by a < canvas> element with a 3D context or accelerated 2D context
  • Layer is used for a composited plugin
  • Layer uses a CSS animation for its opacity or uses an animated webkit transform
  • Layer uses accelerated CSS filters
  • Layer with a composited descendant has information that needs to be in the composited layer tree, such as a clip or reflection
  • Layer has a sibling with a lower z-index which has a compositing layer (in other words the layer is rendered on top of a composited layer)

翻译:

  • RenderLayer 具备 3D 或透视转换的 CSS 属性
  • RenderLayer 包含使用硬件加速的视频解码技术的<video>元素
  • RenderLayer 包含使用硬件加速的 2D 或 WebGL-3D 技术的<canvas>元素
  • RenderLayer 使用了合成插件。
  • RenderLayer 使用了opacitytransform动画
  • RenderLayer 使用了硬件加速的 CSS Filters 技术
  • RenderLayer 后代中包含了一个合成层(若有 clip 或 reflection 属性)
  • RenderLayer 有一个 z-index 比本身小的合成层(即在一个合成层之上)

每一个合成层都有一个 RenderLayerBacking,RenderLayerBacking 负责管理 RenderLayer 所须要的全部后端存储,由于后端存储可能须要多个存储空间。在浏览器(WebKit)中,存储空间使用类 GraphicsLayer 来表示。浏览器会为这些 RenderLayer 建立对应的 GraphicsLayer,不一样的浏览器须要提供本身的 GrphicsLayer 实现用于管理存储空间的分配,释放,更新等等。拥有 GrphicsLayer 的 RenderLayer 会被绘制到本身的后端存储,而没有 GrphicsLayer 的 RenderLayer 它们会向上追溯有 GrphicsLayer 的父/祖先 RenderLayer,直到 Root RenderLayer 为止,而后绘制在有 GrphicsLayer 的父/祖先 RenderLayer 的存储空间,而 Root RenderLayer 老是会建立一个 GrphicsLayer 并拥有本身独立的存储空间。在将每一个合成图层包含的 RenderLayer 内容绘制在合成层的后端存储中,这里绘制能够是软件绘制或硬件绘制。接着由合成器(Compositor)将多个合成层合成起来,造成最终用户可见的网页,实际上就是一张图片。

GraphicsLayer 又构成了一棵与 RenderLayer 并行的树,而 RenderLayer 与 GraphicsLayer 的关系有些相似于 RenderObject 与 RenderLayer 之间的关系。以下是 DOM treeRenderObject treeRenderLayer treeGraphicsLayer tree关系图:

这样能够合并一些 RenderLayer 层,从而减小内存的消耗。其次,合并以后,减小了合并带来的重绘性能和处理上的困难。在硬件加速渲染的合成化渲染和软件绘图的合成化渲染架构下,RenderLayer 的内容变化,只须要更新所属的 GraphicsLayer 的缓存便可,而缓存的更新,也只须要绘制直接或者间接属于这个 GraphicsLayer 的 RenderLayer,而不是全部的 RenderLayer。特别是一些特定的 CSS 样式属性的变化,实际上并不引发内容的变化,只须要改变一些 GraphicsLayer 的混合参数,而后从新混合便可,而混合相对绘制而言是很快的,这些特定的 CSS 样式属性咱们通常称之为是被加速的,不一样的浏览器支持的情况不太同样,但基本上 CSS Transform & Opacity 在全部支持混合加速的浏览器上都是被加速的。被加速的 CSS 样式属性的动画,就比较容易达到 60 帧/每秒的流畅效果了。

不过并非拥有独立缓存的 RenderLayer 越多越好,太多拥有独立缓存的 RenderLayer 会带来一些严重的反作用:

  • 它大大增长了内存的开销,这点在移动设备上的影响更大,甚至致使浏览器在一些内存较少的移动设备上没法很好地支持图层合成加速;
  • 它加大了合成的时间开销,致使合成性能的降低,而合成性能跟网页滚动/缩放操做的流畅度又息息相关,最终致使网页滚动/缩放的流畅度降低,让用户以为操做不够流畅。

Tiled Rendering(瓦片渲染)

一般一个合成层的后端存储被分割成多个大小相同的瓦片状的小存储空间,每一个瓦片能够理解为 OpenGL 中的一个纹理,合成层的结果被分开存储在这些瓦片中。为何使用瓦片化的后端存储?

  • DOM 树种的 html 元素所在的层可能会比较大,由于网页的高度很大,若是只是使用一个后端存储的话,那么须要一个很大的纹理对象,可是实际的 GPU 硬件可能只支持很是有限的纹理大小。
  • 在一个比较大的合成层中,可能只是其中一部分发生变化,根据以前的介绍,须要从新绘制整个层,这样必然产生额外的开销,使用瓦片话的后端存储,就只须要重绘一些存在更新的瓦片。
  • 当层发生滚动的时候,一些瓦片可能再也不须要,而后渲染引擎须要一些新的瓦片来绘制新的区域,这些大小相同的后端存储很容易重复利用。

High Performance Animations(流畅动画)

网页加载后,绘制新的每一帧,通常都须要通过计算布局(layout)、绘图(paint)、合成(composite)三阶段。所以要想提升页面性能(或 FPS),须要减小每一帧的时间。而在这三个阶段中,layout 和 paint 比较耗时间,而合成须要的时间相对较少一些。

layout

若是修改 DOM 元素的 layout 样式(如 width, heihgt 等),浏览器会计算页面须要 relayout 的元素,而后触发一个 relayout。被 relayout 的元素,接着会执行绘制,最后进行渲染合并生成页面。

paint

若是修改 DOM 元素的 paint 样式(如 color, background 等),浏览器会跳过布局,直接执行绘制,再进行合成。

composite

若是修改 DOM 元素的 composite 样式(如 transform, opacity 等)。浏览器会跳过布局和绘制,直接执行合成。该过程是开销最小的,也是优化着手点。

若是想知道修改任何指定 CSS 样式会触发 layout、paint、composite 中的哪个,请查看CSS 触发器

优化

能够经过什么途径进行优化,减小每一帧的时间(避免过多 layout 或 paint):

  • 使用适合的网页分层技术减小 layout 和 paint。一旦有请求更新,若是没有分层,渲染引擎可能须要从新绘制全部区域,由于计算更新部分对 GPU 来讲可能消耗更多的时间。而网页分层以后,部分区域的更新可能只在网页的一层或几层,而不须要将整个网页从新绘制。经过从新绘制网页的一层或几层,并将它们和其余以前绘制完的层合并起来,既能使用 GPU 的能力,又可以减小重绘的开销。
  • 使用合成属性样式(opcity、tansform)来完成 tansition 或 animation。当合成器合成时候,每一个合成层均可以设置变形属性:位移(Translate)、缩放(Scale)、旋转(Rotation)、opacity,这些属性仅仅改变合成层的变换参数,而不须要 layout 和 paint 操做,极大地减小每一帧的渲染时间。

即便用 GPU 硬件加速,为某些 RenderLayer 建立对应 GraphicsLayer。经过为每个合成层设置transform属性来完成 transition 或 animation,有效地避免 relayout 和 repaint 的开销。

总结

本文重点介绍了浏览器渲染引擎的渲染过程,涉及了 DOM treeCSSOM treeRenderObject treeRenderLayer treeGraphicsLayer tree。并对各类渲染模式进行了简单介绍,其中引入了硬件加速机制,还给出一些优化建议。了解这些知识点对咱们开发高性能的 web 应用会有很大的帮助。

参考文章: