WEBKIT 渲染不可不知的这四棵树

3,354 阅读13分钟
原文链接: freeyiyi.com

webkit介绍

浏览器是前端开发者经常打交道的软件,根据 StatCounter 浏览器统计数据目前被广泛使用的五个主流浏览器(Internet Explorer、Firefox、Safari、Chrome)中有三个(chrome、safari、opera)都是webkit内核。移动设备主要是iPhone和安卓,由于AppStore限制了所有app必须使用UIWebView(Apple对WebKit的封装),IOS自不用说,跑着的全是webkit内核,主流安卓手机上的主流浏览器如Android Browser, UC Browser,Opera Mini, Opera Mobile, Chrome等也都是webkit内核。因此开发者最长面对的就是webkit,这也是我们今天要讨论的部分。图为StatCounter统计的201502-201602桌面、移动设备中浏览器使用占比:

StatCounter-browser-ww-monthly-201502-201602

webkit webkit2 chromium blink

之前搞不懂webkit、blink、chromium的关系,查证之后简单跟大家说一下:

  • webkit、webkit2 是开源渲染引擎(排版引擎)、主要贡献者是Apple
  • chromium是Google的浏览器开源项目,主要产品是chrome、canary(chrome的先行版)等产品
  • blink是Chrome从开源webkit中fork出来的分支,Google 剔除了其中与chromium无关的部分,将代码结构重新整理,就目前而言,Blink的渲染和WebKit是一样,但是,以后两者将各自走不同的路

讨论平台

由于webkit移植多,变化快,这里我们只基于mac上chrome和canary讨论,具体版本如下:

OSX EI Capitan 10.11.3
chrome 版本 49.0.2623.75 (64-bit)
canary 版本 51.0.2665.0 canary (64-bit)

渲染过程

根据数据的流向,渲染过程大致分为2个阶段:

从输入URL到DOM

数字表示基本顺序,因为这个过程可能存在重复、交叉,所以也不是严格一致

url to dom

过程如下:

  1. 用户输入URL,webkit调用其资源加载器加载对应的网页
  2. 加载器依赖网络模块建立连接,发送请求并接收回复
  3. webkit接收各种网页或者资源数据,其中某些可能是同步或者异步获取的
  4. 网页被交给HTML解释器,经历以下解析变成DOM结构:Bytes → Characters → Tokens → Nodes → Object Model
  5. 如果遇到js标签,调用js引擎解释并执行,js可能会修改DOM树结构
  6. 如果节点需要依赖其他资源:图片、css、视频等,调用资源加载器异步加载他们,期间不阻碍DOM树的构建,而js资源,则会阻碍DOM树的构建
  7. 网页中依赖的js资源加载完成后,触发DOMContentLoad事件

从DOM到屏幕

css文件下载完成后会被css解释器解释成CSSOM, 并在DOM树上附加解释后的样式信息,构建RenderObject树,即:Render Tree。

  1. webkit会根据网页的层次结构创建RenderLayer树,处理诸如z-index、浮动、定位等布局
  2. 浏览器之后会将每个RenderLayer栅格化,并独立的绘制进位图中,将这些位图作为纹理上传至 GPU,复合多个层来生成最终的屏幕图像(终极layer: cc layers(cc = Chrome compositor))

  3. 对于软件渲染而言,到这里就跳到步骤6

  4. 对于硬件渲染来说,webkit还为某些符合条件的元素创建GraphicsLayer,把他们的后端信息上传到GPU中处理,这样在这些元素的透明度、位移等发生改变的时候就不会触发重绘,而是直接使用GPU保存的数据重新复合

  5. webkit通过显卡,把内容绘制到屏幕上。RenderLayer递归的绘制自己和子女,RenderObject则会按照背景和边框、浮动内容、内容区前景这样的顺序绘制自己。

  6. 第一次绘制的绘制区域是可视区的大小,会触发first paint事件。这一过程不必等到整个 HTML 文档解析完毕之后才进行,first paintDOMContentLoad无先后关系

  7. 所有依赖资源加载完毕触发onload事件

  8. 在渲染完成后,由于动画、用户交互、图片、视频、音频加载完成,浏览器会重复执行渲染过程

上述五个内部结构: DOMCSSOMRenderTreeRenderLayer GraphicsLayer一直同时存在于内存中,直到网页标签被关闭,进程被销毁。

webkit渲染视频 17:35分钟左右,当然十分推荐看完整个视频,如果无法翻墙,请看这个

知道了大概的渲染流程,我们来看看过程中涉及的几颗重要的树结构,以及他们在渲染中起到的重要作用。

四棵树结构

DOM Tree

经历以下解析变成DOM结构:Bytes → Characters → Tokens → Nodes → Object Model, 每个node节点都有一个attach方法,方便后续根据CSSOM附加相应的样式信息。

full-process

值得说明的是: 1. 在html解析完之前,即DOM树没构建完成之前,若CSSOM解析完成,CSSOM解释器的结果会保存起来 1. DOM 树的构建过程是一个深度遍历过程,即当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点

RenderObjects Tree

RenderObject对象在DOM树创建的同时也会被创建,如果DOM中有动态加入元素时,也可能会相应地创建RenderObject对象。下图示例的是RenderObject对象被创建的函数调用过程:

node to RenderObject

RenderObject是一个基类(RenderObject.h),他还有很多派生子类。RenderObject常用的类的继承关系图:

RenderObject派生类

每个RenderObject都有layout方法,Render树建立之后,布局运算会计算出诸如位置,大小,是否浮动等样式布局信息。有了这些信息之后,渲染引擎才只知道在何处以及如何画这些元素。

DOM中可见元素和RenderObject是一对一的关系,等不可见标签,display: none 的元素不会出现在Render Tree里, 而visibility: hidden会在 Render Tree里(见第4张图)。 另外注意:

  • 根据 CSS 规范,inline 元素只能包含 block 元素或 inline 元素中的一种。如果包含多种,会自动创建一个匿名盒模型,这个盒模型也对应一个Anonymous RenderObject。
  • 另外虽然js没法访问影子DOM,但是webkit需要创建并渲染RenderObject

AnonymousRenderObjects

Render tree 的判定

  • DOM树的document节点;
  • DOM树中的可视化节点,例如HTML,BODY,DIV等,非视可化节点不会建立RenderObject节点,例如HEAD,META,SCRIPT等;
  • 某些情况下需要建立匿名的RenderObject节点;

RenderLayers Tree

我们知道,任何元素都可以使用overflow属性,webkit可能需要为滚动的内容、容器、滚动条建立新层,还有剪裁层和反射层,Z正、Z负的层,这些层次是如何组织,它们的绘制顺序又是如何呢?

为了网页以正确的层次覆盖关系展示出来,webkit会把共享一个坐标空间的RenderObjects合并成一个RenderLayer,如:含有transform属性的元素因为transform会为自己创建一个独特的坐标系。所以,RenderObjects 和 RenderLayers是多对一的关系。另外,根元素也是一个RenderLayer,这样,它们也构成了一颗RenderLayer树。

dom to render tree to renderlayer tree

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 element that has a 3D (WebGL) context or an accelerated 2D context
  • Corresponds to a element

翻译过来就是:

  • 根元素
  • 有明确定位属性(position 不为static或者有transform属性)
  • 有透明效果
  • 有 overflow, alpha mask 或者reflection效属性
  • 有 CSS filter
  • Canvas 2D和3D (WebGL)
  • Video节点

为了可视化理解RenderLayer Tree,我从《webkit技术内幕》中摘录了一个demo

源代码 html

abc
d0

这段HTML源代码被WebKit解析后会生成一颗DOM树。当DOM树生成时,WebKit同时建立了一颗Render树,代码的Render树被打印成如下图所示的文本信息:

上图中的layer at (x, x)表示不同的RenderLayer节点,下面的所有RenderObject均属于该RenderLayer。

第一个layer对应DOM中的Document节点。后面的(0, 0)表示该节点在坐标系中的位置,最后的800x600表示该节点的大小。 第二个layer,包含了HTML中的绝大部分元素。这里有三点需要解释一下:

  • Head元素没有相应的RenderObject,因为它不是一个可视的元素
  • Canvas元素不在其中,而是在第三个layer中(RenderHTMLCanvas)。但是它仍然是RenderBody节点的子女
  • 匿名的RenderBlock节点,它包含了RenderText, RenderInline等节点,原因如前所说。

第三个layer。因为从Canvas创建了一个WebGL3D Context对象,这里需要重新生成一个新的layer。

最后,来说明一下三个layer的创建时间。第一和第二个layer在创建DOM树后,会触发创建;第三个layer测试资源加载解析好之后,执行后面的JavaScript代码后所创建。

想查看更多实例test可以去这里webkit/LayoutTests/

点击或者搜索诸如RenderHtmlCanvas之类的关键字,可以查看到诸如这样的测试用例

GraphicsLayers Tree

浏览器将DOM分割为多个层(RenderLayer)之后会将每个层栅格化,并独立的绘制进位图中,将这些位图作为纹理上传至 GPU,复合多个层来生成最终的屏幕图像(终极layer: cc layers(cc = Chrome compositor))。

如果是软件渲染的话,到这里就结束了。当遇到会改变元素的透明度的动画,我们知道这些是会更新RenderLayer的,所以webkit会重新layout、repaint到位图,上传到GUP,再composite生成最终屏幕上的图像。

软件渲染没有为每一个RenderLayer提供后端存储,如果对一个RenderLayer更新,他需要将和这个RenderLayer所有重叠部分的相关区域的RenderObjects从后向前重绘一遍。可想而知如果更新区域包含CSS3D、HTML5多媒体、WebGL和video等,那将是一场灾难。webkit提供一种机制,可以给某些特殊属性的RenderLayer提供后端存储,如果改变的是透明度、位移、缩放、变形等并不需要发生重绘,可以使用早已作为纹理而存在于 GPU 中的层来重新复合,避开软件渲染在高级绘制方面的不足,充分利用GPU在处理图像方面的优势。在WebKit中,RenderLayer的存储空间使用类GraphicsLayer来表示,也有人称复合层(compositing layers)。尽管每个RenderLayer理论上都可以有自己的后盾存储,但是这个做会造成资源浪费 性能反而不好。

webkit中满足下列条件的RenderLayer会有属于自己的后端存储,即GraphicsLayers(更多详情请参考CompositingReasons.cpp这里显示了所有RenderLayer可以成为GraphicsLayer的原因)

  • RenderLayer has 3D or perspective transform CSS properties
  • RenderLayer is used by element using accelerated video decoding
  • RenderLayer is used by a element with a 3D context or accelerated 2D context
  • RenderLayer is used for a composited plugin
  • RenderLayer uses a CSS animation for its opacity or uses an animated webkit transform
  • RenderLayer uses accelerated CSS filters
  • RenderLayer has a descendant that is a compositing layer
  • RenderLayer has a sibling with a lower z-index which has a compositing layer (in other words the layer overlaps a composited layer and should be rendered on top of it)

翻译:

  • RenderLayer具有CSS 3D属性或者CSS透视效果。
  • RenderLayer包含的RenderObject节点表示的是使用硬件加速的视频解码技术的HTML5 video元素。
  • RenderLayer包含的RenderObject节点表示的是使用硬件加速的Canvas2D元素或者WebGL技术。
  • RenderLayer使用了CSS透明效果的动画或者CSS变换的动画。
  • RenderLayer使用了硬件加速的CSSfilters技术。
  • RenderLayer的后代中包括了一个合成层。
  • 合成层上面(z轴上距离读者更近)的RenderLayer也是一个合成层

幸福的是,chrome为开发者提供了很好的工具便于我们可视化地理解GraphicsLayer。

打开示例layer demo

 class="circle">
   class="square">
  

css: body { margin: 0; background: #eee; } .circle { position: relative; margin: 80px auto; width: 200px; height: 200px; background: #123456; border-radius: 50%; overflow: hidden; -webkit-transform: translate3d(0,0,0); /*outline: 1px solid;*/ /* -webkit-transform: matrix(1, 0, 0, 1, 0, 0); */ /* -webkit-transform: skew(0); */ /* -webkit-transform: scale(1); */ /* -webkit-transform: translate(0); */ /* -webkit-transform: translateX(0); */ /* -webkit-transform: translate(0px, 0px); */ /*z-index: 0;*/ /* -webkit-transform: rotate(0deg); */ /* -webkit-transform: translateZ(0); */ } .square { position: absolute; left: 10px; top: 10px; width: 80px; height: 80px; background: #fff; } @keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .circle:hover .square { animation: rotate 5s linear infinite; }

打开console查看页面:

layer

过程如下:

  1. 按下ESC打开面板1,并勾选show layer border即可看到我们设置了transform: scale3d(0, 0, 0)的白色方块和document这两个层周围的黄色边框。
  2. 打开timeline面板
  3. 点击小圆点开始收集,如果之前timeline里有内容,需要点他旁边的小圆点清空一下
  4. 鼠标悬浮在我们的白色方块上,方块开始旋转
  5. 选择要研究的区间
  6. 点击6中的小色块,并左右移动
  7. 观察7里的信息
  8. 木有8、9

观察7,我们会发现小白方块在旋转的过程中没有layout、repaint即重排重绘(关于重排重绘,可以餐开这篇文章)只有复合层composite layers这一过程。

layer demo2

  1. 我是上面的9
  2. 高能预警,这里有魔法!点我可以看到这个阶段网页各个渲染阶段的所有层的情况哦~
  3. 看这里,这里是layer tree
  4. 这里是层的大小(80 * 80),层相对层坐标系坐标原点的位置(at 10, 10)以及层形成的原因等
  5. 图片上没有标出来,在3和4中间,上面有三个按钮,分别可以拖动、3d查看、重置

总结

我们一共介绍了为了不同的渲染目的,四棵同时存在在内存中的树结构:

  • DOM树: html代码下载完后解析的结果,包含了所有HTML标签,包括display:none的隐藏标签,还有用JS动态添加的元素等
  • Render树: RenderObject组成的树结构,RenderObject和DOM结构的Node可视节点基本上是一对一的关系,CSSOM生成后和DOM树合成的树结构,DOM树知道如何绘制自己,但是要注意特殊情况。
  • RenderLayer树: 由RenderLayer组成,RenderLayer和RenderObject是一对多的关系,RenderLayer主要负责网页的层次关系
  • GraphicsLayer树: 由GraphicsLayer组成,GraphicsLayer和RenderLayer是一对多的关系,GraphicsLayer主要负责硬件渲染

PS: 四棵树的时间关系

浏览器下载完html后开始将其解析成DOM树,解析过程是深度遍历,即解析完当前DOM所有子节点,才会开始解析当前DOM的下一个节点,CSSOM树会等待DOM树构建完成,然后DOM节点的attach方法将CSS样式信息附加到DOM节点上形成Render树,附加是同步进行的,Render Tree在创建的同时,webkit会根据网页的层次结构创建RenderLayer树,如有元素有能开启硬件加速的属性,chrome会为他创建GraphicsLayer,同时由于根元素也是GraphicsLayer,所以GraphicsLayer也构成一颗树的结构。DOM树改变后,webkit会重复以上渲染过程。

思考

  • DOM中还有别的结构,硬件加速会怎么处理?
  • DOM中内容发生变化会触发重绘嘛?
  • 哪些属性可以开启硬件加速,而不会对元素产生任何影响?
  • 那么只是知道使用诸如transform: translate3d(0, 0, 0)开启硬件加速提高网页性能这句话是不是足够了呢?

参考

  1. how browser work | 中文版
  2. webkit for developers | 开发者应当了解的WebKit知识
  3. 浏览器市场份额占比
  4. Render-tree construction, layout, and paint
  5. Constructing the Object Model
  6. JS 一定要放在 Body 的最底部么?聊聊浏览器的渲染机制
  7. 朱勇盛blog
  8. GPU Accelerated Compositing in Chrome
  9. Javascript高性能动画与页面渲染
  10. webkit source code
  11. Accelerated Rendering in Chrome
  12. 浏览器的渲染原理简介
  13. 《webkit技术内幕》