webkit 简介
首发于2003年的safari浏览器。WebKit的代码源自1998年所开发的KDE的HTML排版引擎KHTML及KDE的JavaScript引擎KJS的代码, 当时webkit仅为khtml与kjs的一个fork版本,将他们重新命名为 WebCore 及 JavaScriptCore。 之后因为各平台需要发展出不同的分支,像chromium,qtwebkit等
webkit组成部分
见下图:

- browser UI比如浏览器的书签栏搜索栏等
- webkit Embedding API是webkit嵌入的api接口,browser UI通过webkit Embedding API与webpage进行交互。
- webcore 负责webpage的资源的调度加载, 解析, cssobject model构建, dom解析与构建, 事件处理等
- jacascriptCore 包括垃圾回收与解释器(将js语法转为二进制机器码)
- platformAPI是提供与底层驱动的交互, 如网络, 字体渲染, 影音文件解码等, 渲染引擎(webkit将所有的render都交给platform处理, 值得一提的是不同的浏览器会选择不同的渲染引擎, 想chrome为了保持不同平台尽可能的一致,采用的是skia, 而安卓是完全交给android stack来处理的,这也是为什么不同款的安卓机展示的出的ui有些是不同的)
webkit工作流程
具体流程见下图:

webkit的资源加载器对加载到的html,css,js分别解析。 后经过layout渲染出render tree, 经过绘制(paint),合成(composite) 最终输出到浏览器上, 我们简单对下面代码进行解析:
index.html
<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>
style.css
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
htmlParser解析构建dom tree
dom解析需要经过以下步骤
- 转换:浏览器从磁盘或网络读取 HTML 的原始字节,然后根据指定的文件编码格式(例如 UTF-8)将其转换为相应字符。
- 符号化:浏览器将字符串转换为 W3C HTML5 标准 指定的各种符号 - 比如 ““、”” 及其他「尖括号」内的字符串。每个符号都有特殊含义并一套规则。
- 词法分析:发射的符号转换为「对象」,定义它们的属性与规则。
- DOM 构建:最后,因为 HTML 标记定义不同标签间的相互关系(某些标签嵌套在其他标签中),所以创建的对象在树状数据结构中互相链接,树状数据结构还捕获原始标记中定义的父子关系:比如 HTML 对象是 body 对象的父对象,body 是 paragraph 对象的父对象等等。

cssParse解析成cssOM(树状结构)
与构建dom经过相同的步骤, 结果如下

经过layout/reflow构建出render tree
大致步骤如下:
- 从 DOM 树的根节点开始,遍历每个可见的节点。
- 某些节点完全不可见(例如 script 标签、meta 标签等),因为它们不会在渲染结果中反映,所以会被忽略。
- 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略。比方说,上面例子中的 span 节点,因为该节点有一条显式规则设置了 display:none 属性,所以不会出现在渲染树中。
- 给每个可见节点找到相应匹配的 CSSOM 规则,并应用这些规则。
- 发射可见节点,连带其内容及计算的样式。
构建过程如下:

关于批量dom优化在layout/reflow过程可能产生的layout thrashing问题可以参考我的另一篇blog DOM批量处理与 layout thrashing
paint过程简介 与 compsoite硬件加速
paint简介
由webkit工作流程图可知, 当我们触发layout层的时候(修改dom布局的几何属性时触发,具体参考我的另一篇blog DOM批量处理与 layout thrashing ),必然会触发paint层或repaint层。触发一些非布局几何属性例如background, box-shdow等也会触发。paint过程是填充像素的过程,这些像素将最终显示在用户的屏幕上。通常这个过程是最消耗时长的一环。
绘制过程并非单层绘制,而是多层绘制后并合并成渲染层,最后合并为一层。以下列出了产生新的渲染层的条件:
- 3D 或透视变换(perspective transform) CSS 属性
- 使用加速视频解码的 元素
- 拥有 3D (WebGL) 上下文或加速的 2D 上下文的 元素
- 混合插件(如 Flash)
- 对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素
- 拥有加速 CSS 过滤器的元素
- 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
- 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
默认渲染层是使用GPU加速的,就意味着当cpu计算完成paint过程之后会传给gpu存储,处理。我们知道gpu擅长图片处理尤其是一些matrix的变换,translate, skew, alpha等。第一次时cpu计算完成传给gpu处理后改变下次变化matrix的变化不需要触发paint/repaint过程,直接触发composite过程即可,这样就避免了repain的大量的冗余的计算。 具体过程可以通过如下:
最好方式就是使用CSS属性will-change,Chrome/Opera/Firefox都支持该属性
.moving-element {
will-change: transform;
}
或者,对于旧版本或不支持will-change属性的浏览器通过 translateZ(0) 或者 opacity < 1:
.moving-element {
transform: translateZ(0);
}
创建一个新的渲染层并不是免费的,它得消耗额外的内存和管理资源。实际上,在内存资源有限的设备上,由于过多的渲染层来带的开销而对页面渲染性能产生的影响,甚至远远超过了它在性能改善上带来的好处。由于每个渲染层的纹理都需要上传到GPU处理,因此我们还需要考虑CPU和GPU之间的带宽问题、以及有多大内存供GPU处理这些纹理的问题。
参考资料
推荐阅读