深入理解浏览器

介绍

作为前端开发者,我们知道硬件加速(hardware acceleration)能够提升页面的渲染性能,提高动画的流畅度。也知道可以通过设置transformCSS3属性来手动开启硬件加速,也就是GPU加速。本文将介绍在 Chrome 中,为 web 内容提供硬件加速的基本模型。

申明

本文中讨论的内容都是基于 Chrome 浏览器,也就是 WebKit 内核,更准确的说,是讨论 WebKit 的 Chromium 分支。本文描述的是 Chrome 的实现细节,而并非是 web 平台的功能。web 平台和标准不会对这种层面的实现细节制定规则,因此文中介绍的内容不一定适用于其他浏览器,但是了解内部实现对于高级调试和性能优化有着不小的帮助。

书写时:Chrome: 版本 74.0.3729.169(正式版本) (64 位)

由于AppStore限制了所有app必须使用UIWebView(Apple对WebKit的封装),IOS自不用说,跑> 着的全是webkit内核,主流安卓手机上的主流浏览器如 Android Browser, UC Browser,Opera Mini, Opera Mobile, Chrome 等也都是 webkit 内核。

浏览器发展简史

想看更清晰的,可以前往:图片原址

更多详细信息,可以去谷歌一下,蛮多的,下面是一些看上去整理的还不错的信息:

  1. 野史类 小文:浅谈浏览器发展简史(中文)
  2. 知识类 Timeline_of_web_browsers(英文)

浏览器的基本结构大抵如下图:

Webkit 渲染基础

要提供快速的网络体验,浏览器需要做许多工作。这类工作大多数是我们这些网络开发者看不到的:我们编写代码(HTMLCSSJavaScript ),屏幕上就会显示出漂亮的页面。

浏览器中页面的渲染过程可以简化为以下五个步骤:

而浏览器渲染页面前需要先构建 DOMCSSOM 树。因此,我们需要确保尽快将 HTMLCSS 都提供给浏览器。

浏览器处理页面流程

首先来看下一个简单的页面(一个包含一些文本和一幅图片的普通 HTML 页面)

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE 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>

浏览器如何处理此页面?

DOM 的建立

  1. 转换: 浏览器从磁盘(缓存)或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成各个字符。
  2. 标签化: 浏览器将字符串转换成 W3C HTML5 标准规定的各种标签,例如,“<html>”、“<body>”,以及其他尖括号内的字符串。每个标签都具有特殊含义和一组规则。
  3. 词法分析: 将标签转换成“对象”,用来定义其属性和规则
  4. DOM 构建: 根据原来的 HTML 标签的结构构建成一颗树,记录原始标签之间的关系:HTML 对象是 body 对象的父项,bodyparagraph 对象的父项,依此类推。

整个流程的最终输出是我们这个简单页面的文档对象模型 (DOM),浏览器对页面进行的所有进一步处理都会用到它。

浏览器处理 HTML 标签,都会经过上述4个步骤:转换、标签化、词法分析、DOM 构建。

CSSOM 的建立

在浏览器构建我们这个简单页面的 DOM 时,在文档的 head 部分遇到了一个 link 标记,该标记引用一个外部 CSS 样式表:style.css。由于预见到需要利用该资源来渲染页面,它会立即发出对该资源的请求,并返回以下内容:

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

浏览器处理 CSS 的这个过程,会重复 DOM 的建立过程,只是输出的是 CSSOM

CSS 字节转换成字符,接着转换对应标签和节点,最后链接到一个称为“CSS 对象模型”(CSSOM) 的树结构内:

CSSOM 为何具有树结构?因为页面上的任何对象计算最后一组样式时,浏览器都会先从适用于该节点的最通用规则开始(例如,如果该节点是 body 元素的子项,则应用所有 body 样式),然后通过应用更具体的规则(即规则“向下级联”)以递归方式优化计算的样式。

以上面的 CSSOM 树为例进行更具体的阐述。span 标记内包含的任何置于 body 元素内的文本都将具有 16 像素字号,并且颜色为红色 — font-size 指令从 body 向下级联至 span。不过,如果某个 span 标记是某个段落 (p) 标记的子项,则其内容将不会显示。

还请注意,以上树并非完整的 CSSOM 树,它只显示了我们决定在样式表中替换的样式。每个浏览器都提供一组默认样式(也称为“User Agent 样式”),即我们不提供任何自定义样式时所看到的样式,我们的样式只是替换这些默认样式。

合并渲染

CSSOM 树和 DOM 树合并成渲染树,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。

1. 构建渲染树阶段

第一步是让浏览器将 DOM 和 CSSOM 合并成一个“渲染树”,网罗网页上所有可见的 DOM 内容,以及每个节点的所有 CSSOM 样式信息。

为构建渲染树,浏览器大体上完成了下列工作:

  • 从 DOM 树的根节点开始遍历每个可见节点。
    • 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
    • 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如,上例中的 span 节点不会出现在渲染树中,因为有一个显式规则在该节点上设置了“display: none”属性。(注意display: nonevisibility: hidden的区别,前者不在渲染树上,后者是存在的,只是不可见罢了)
  • 对每个可见节点,找到适配的 CSSOM 规则。
  • 放置准备好的节点(内容和计算的样式)。
2. 布局阶段(重排 Reflow)

浏览器从渲染树的根节点进行遍历,获取每个节点的确切大小和位置,然后再进行布局,看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<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 将节点的显示尺寸设置为视口宽度的 50%
  • div 包含的第二个 div => 将其宽度设置为其父项的 50%,即视口宽度的 25%

布局流程的输出是一个“盒模型”(注意标准盒模型和IE盒模型的区别),它会精确地捕获每个元素在视口内的确切位置和尺寸:所有相对测量值都转换为屏幕上的绝对像素。

3. 绘制阶段(栅格化)

将渲染树中的每个节点转换成屏幕上的实际像素,这就是浏览器的绘制阶段了,或者说是栅格化阶段。

总结(浏览器处理页面流程)

  1. 构建DOM 树阶段:处理 HTML 标记并构建 DOM 树。
  2. CSSOM 树阶段:处理 CSS 标记并构建 CSSOM 树。
  3. 合并渲染树阶段:将 DOM 与 CSSOM 合并成一个渲染树。
  4. 布局(重排)阶段:根据渲染树来布局,以计算每个节点的几何信息。
  5. 绘制(栅格化)阶段:将各个节点绘制到屏幕上。

当页面加载并解析完毕后,它在浏览器内代表了一个大家十分熟悉的结构:DOM。在浏览器渲染一个页面时,它使用了许多没有暴露给开发者的中间表现形式。其中最重要的结构便是层(layer)。

Chrome 中有不同类型的层: RenderLayer(负责 DOM 子树),GraphicsLayer(负责 RenderLayer 的子树)。其中只有 GraphicsLayer 是作为纹理(texture)上传给 GPU 的。

从 DOM 到 RenderObject

RenderObject 是衔接浏览器排版引擎和渲染引擎之间的桥梁,它是排版引擎的输出和渲染引擎的输入。当 Webkit 创建 RenderObject 对象之后,每个对象是不知道自己的位置、大小等信息的,Webkit 根据框模型来计算它们的位置、大小等信息的过程称为布局计算。

从 RenderObject 到 RenderLayer

Webkit 会为网页的层次创建相应的 RenderLayer 对象,当某些类型的 RenderObject 的节点或者具有某些 CSS 样式的 RenderObject 节点出现的时候,Webkit 就会为这些节点创建 RenderLayer 对象,一般来说某个 RenderObject 节点的后代都属于该节点的 RenderLayer,除非 Webkit 根据规则为某个后代 RenderObject 节点创建一个新的 RenderLayer 对象,以下是 RenderObject 节点需要建立新的 RenderLayer 节点的规则:

  • DOM 树的 document 节点对应的 RenderView 节点
  • DOM 树中 document 的子女节点,即 html 节点对应的 RenderBlock 节点
  • 显示指定 CSS 位置的 RendrObject 节点
  • 有透明效果的 RenderObject 节点
  • 节点有溢出(overflow)、alpha 或者反射等效果的 RenderObject 节点
  • 适用 canvas 2d 或者 3d(WebGL)技术的 RenderObject 节点
  • video 节点对应的 RenderObject 节点

软件渲染和硬件加速渲染

在 Webkit 中绘图操作被定义为一个抽象层即绘图上下文,所有绘图操作都是在该上下文中进行,可以分为两种类型:2d 图形上下文和 3d 图形上下文。其中 2d 图形上下文的具体作用就是提供基本绘图单元的绘制接口以及设置绘图的样式,绘图接口包括画点、画线、画图片、画多边形、画文字 etc.,绘图样式包括颜色、线宽、字号大小、渐变 etc.,而RenderObject 对象知道自己需要画什么样的点,什么样的图片。3d 绘图上下文的主要用处是支持 CSS3D、WebGL etc.。

网页的渲染方式主要有两种:软件渲染和硬件加速渲染。每个 RenderLyaer 对象都可以被想象成一个层,各个层一同构成一个图像,在渲染过程中,每个层对应网页中的一个或者一些可视元素,这些元素都绘制内容到该层上,如果这些绘图操作由 CPU 莱完成则称之为软件绘图,如果这些绘图操作由 GPU 来完成则称之为硬件加速绘图。理想情况下,每个层都有绘制的存储区域来保存绘图的结果,最后需要将这些层的内容合并到同一个图像中的过程称为合成(compositing),使用合成技术的渲染叫做合成化渲染。

对于软件渲染机制,Webkit 需要使用 CPU 来绘制每层的内容,然而该机制是没有合成阶段的:在软件渲染中通常其结果就是一个位图(Bitmap),绘制每一层时都使用同一个位图,区别在于绘制的位置看你不一样,每一层都按照从后到前的顺序。而使用合成化的渲染技术,以使用软件绘图的合成化渲染为例,对于使用 CPU 绘制的层,其结果保存在 CPU 内存中,之后传输到 GPU 中进行合成。

从 RenderLayer 到 GraphicsLayer

每个 GraphicsLayer 都拥有一个 GraphicsContext,用于为该 GraphicsLayer 开辟一段位图,也就意味着每个 GraphicsLayer 都拥有一个独立的位图,GraphicsLayer 负责将自己的 RenderLayer 及其所包含的 RenderObject 绘制到位图里,然后将位图作为纹理交给 GPU 进行合成。如果一个 RenderLayer 对象具有以下特征之一,那么它就是合成层:

  • RenderLayer 具有 CSS3D 属性或者 CSS 透视效果
  • RenderLayer 包含 video 节点对应的 RenderObject 节点
  • RenderLayer 包含使用 canvas 2d 或者 3d(WebGL)技术的 RenderObject 节点
  • 混合插件(如 Flash)
  • RenderLayer 使用 CSS 透明效果的动画或者 CSS 变换动画
  • RenderLayer 使用硬件加速的 CSS Filters 技术
  • 元素有一个包含合成层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
  • 元素有一个 z-index 较低且包含一个合成层的兄弟元素(换句话说就是该元素在复合层上面渲染)

什么是纹理
纹理其实就是 GPU 中的位图,存储在 GPU video RAM 中。前面说的位图里的元素存什么我们自己定义好就行(是用3字节存256位rgb还是1个bit存黑白自己定义即可),但纹理是 GPU 专用的,需要有固定格式便于兼容与处理,所以一方面纹理的格式比较固定,如 R5G6B5、A4R4G4B4 等像素格式, 另外一方面 GPU 对纹理的大小有限制,比如长/宽必须是2的幂次方,最大不能超过2048或者4096等。

合成层的规则细分:

元素本身原因

  • 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层)
  • video 元素
  • 覆盖在 video 元素上的视频控制栏
  • 3D 或者 硬件加速的 2D Canvas 元素
  • 硬件加速的插件:比如 flash etc.
  • 在 DPI 较高的屏幕上 fix 定位的元素会自动地被提升到合成层中;但在 DPI 较低的设备上却并非如此:因为这个渲染层的提升会使得字体渲染方式由子像素变为灰阶
  • 有 3D transform
  • backface-visibility 为 hidden
  • 对 opacity、transform、fliter、backdropfilter 应用 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
  • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性:比如 relative etc.)

后代元素原因

  • 有合成层后代同时本身有 transformopactiy(小于 1)maskfliterreflection 属性
  • 有合成层后代同时本身 overflow 不为 visible(如果本身是因为明确的定位因素产生的 SelfPaintingLayer,则需要 z-index 不为 auto
  • 有合成层后代同时本身 fixed 定位
  • 有 3D transfrom 的合成层后代同时本身有 preserves-3d 属性
  • 有 3D transfrom 的合成层后代同时本身有 perspective 属性

重叠原因

重叠或者说部分重叠在一个合成层之上,最常见和容易理解的就是元素的 border box(content + padding + border) 和合成层的有重叠,其他的还有一些不常见的情况,也算是同合成层重叠的条件如下:

  • filter 效果同合成层重叠
  • transform 变换后同合成层重叠
  • overflow scroll 情况下同合成层重叠

假设重叠在一个合成层之上,其实也比较好理解,比如一个元素的 CSS 动画效果在运行期间,元素有可能和其他元素发生重叠的情况,需要注意的是该原因下,有一个很特殊的情况:如果合成层有内联的transform 属性,会导致其兄弟渲染层假设重叠从而提升为合成层。

由于重叠的原因可能随随便便就会产生出大量合成层来,而每个合成层都要消耗 CPU 和内存资源,会严重影响页面性能。

层压缩

层压缩(Layer Squashing)的处理。如果多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止由于重叠原因导致可能出现的“层爆炸”。

当然,浏览器的自动层压缩也不是万能的,在很多特定情况下,浏览器是无法进行层压缩的,而这些情况也是我们应该尽量避免的(以下情况都是基于重叠原因而言):

  • 无法进行会打破渲染顺序的压缩
  • video 元素的渲染层无法被压缩,同时也无法将别的渲染层压缩到 video 所在的合成层上
  • iframeplugin 的渲染层无法被压缩,同时也无法将别的渲染层压缩到其所在的合成层上
  • 无法压缩有 reflection 属性的渲染层
  • 无法压缩有 blend mode 属性的渲染层
  • 当渲染层同合成层有不同的裁剪容器时,该渲染层无法压缩
  • 相对于合成层滚动的渲染层无法被压缩
  • 当渲染层同合成层有不同的具有 opacity 的祖先层(一个设置 opacity 且小于 1 一个没有设置 opacity 也算是不同)时,该渲染层无法压缩
  • 当渲染层同合成层有不同的具有 transform 的祖先层时,该渲染层无法压缩
  • 当渲染层同合成层有不同的具有 filter 的祖先层时,该渲染层无法压缩
  • 当覆盖的合成层正在运行动画时,该渲染层无法压缩,只有在动画未开始或者运行完毕以后,该渲染层才可以被压缩

重排&重绘

重排

如果改变了一个影响元素布局信息的 CSS 样式:比如 widthheightlefttop etc.,那么浏览器会将当前的 Layout 标记为 dirty,这会使得浏览器在下一帧执行重排,因为元素的位置信息发生改变将可能会导致整个网页其他元素的位置情况都发生改变,所以需要执行 Layout 全局重新计算每个元素的位置。

强制重排

如果你在当前 Layout 被标记为 dirty 的情况下访问 offsetTopscrollHeight 等属性,那么浏览器会立即重新 Layout,计算出此时元素正确的位置信息,以保证你在 JS 里获取到的 offsetTopscrollHeight 等是正确的。

这一过程被称为强制重排 Force Layout,强制浏览器将本来在渲染流程中才执行的 Layout 过程提前至 JS 执行过程中,每次当我们在 Layout 为 dirty 时访问会触发重排的属性都会 Force Layout,这会极大延缓 JS 的执行效率。

另外,每次重排或者强制重排后,当前 Layout 就不再 dirty,这时再访问 offsetWidth 之类的属性并不会再触发重排。

重绘

一旦更改某个元素的会触发重绘的样式,那么浏览器就会在下一帧的渲染步骤中进行重绘(也即一些介绍重绘机制中说的invalidating),JS 更改样式导致某一片区域的样式作废,从而在一下帧中重绘 invalidating 的区域。

重绘是以合成层为单位的,也即 invalidating 的既不是整个文档也不是单个元素,而是这个元素所在的合成层。当然这也是将渲染过程拆分为 PaintCompositing 的初衷之一:

Since painting of the layers is decoupled from compositing, invalidating one of these layers only results in repainting the contents of that layer alone and recompositing.

回忆下浏览器的渲染步骤图:

TIP:如果想要知道修改了元素属性,到底是触发重排还是重绘,网上有作出了总结:传送门 ,如果不能访问,请科学上网。
部分截图:

Chrome 如何计算合成层可以操考如下图:

浏览器渲染流程可以简化总结如下:

GPU加速的实际意义

动画

最常见的一个就是动画了,开启 GPU 加速对动画流畅度的优化程度,是肉眼可见的,但是,切记谨慎开启GPU加速,而不要一股脑的全部开启,那样会卡成幻灯片的。

滚动

来个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Composited layer</title>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<style type="text/css">
body {
font-family: 'Helvetica Neue', 'Microsoft Yahei', sans-serif;
}
@-webkit-keyframes move {
0% { -webkit-transform:translateX(0px) }
50% { -webkit-transform:translateX(10px) }
100% { -webkit-transform:translateX(0px) }
}
@-moz-keyframes move {
0% { -moz-transform:translateX(0px) }
50% { -moz-transform:translateX(10px) }
100% { -moz-transform:translateX(0px) }
}
@-o-keyframes move {
0% { -o-transform:translateX(0px) }
50% { -o-transform:translateX(10px) }
100% { -o-transform:translateX(0px) }
}
@keyframes move {
0% { transform:translateX(0px) }
50% { transform:translateX(10px) }
100% { transform:translateX(0px) }
}
#title {
-webkit-animation: move 1s linear infinite;
-moz-animation: move 1s linear infinite;
-o-animation: move 1s linear infinite;
animation: move 1s linear infinite;
/*position: relative;
z-index: 1;*/
}
h1 {
font-size: 20px;
}
a {
color: #888;
text-decoration: none;
}
div {
overflow: hidden;
}
ul {
margin: 5px;
padding: 0;
}
li {
position: relative;
height: 30px;
overflow: hidden;
border-top: 1px dotted #ccc;
padding: 5px 20px 5px 40px;
font-size: 20px;
}
img {
width: 30px;
height: 30px;
position: absolute;
left: 3px;
top: 4px;
border: 1px solid #ccc;
}
time {
position: absolute;
right: 5px;
top: 4px;
font-size: 12px;
color: #999;
}
label {
display: block;
margin: 10px 5px;
color: red;
}
input {
vertical-align: middle;
}
</style>
</head>
<body>
<div>
<h1 id="title">请使用具备『硬件加速』功能的『安卓』手机浏览此页面</h1>
<label>
<input type="checkbox" onchange="setZIndex(this)"> 为动画元素设置z-index
</label>
</div>
<ul id="list"></ul>
<script>
// 处理query
var query = (function(query){
var q = {};
query = query.replace(/^\?+/, '');
if(query){
query.split('&').forEach(function(i){
i = i.split('=');
q[i[0]] = i[1];
});
}
return q;
})(location.search);
</script>
<script>
var $ = function(selector){
return document.querySelector(selector);
};
</script>
<script>

var setZIndex = function(checkbox){
var title = $('#title');
if(checkbox.checked){
title.style.position = 'relative';
title.style.zIndex = 1;
} else {
title.style.position = 'static';
}
};
// 生成DOM
var template = function(i){
return [
'<li class="album-item">',
'<img src="assets/' + (i % 16) + '.png"/>',
'hello world',
'<time>2015-09</time>',
'</li>'
].join('');
};
var size = parseInt(query.size) || 2000;
var html = '';
for(var i = 0; i < size; i++){
html += template(i);
}
$('#list').innerHTML = html;
</script>
</body>
</html>

体验下两种不同的情况下,滚动的流畅程度,也是肉眼可见的,只修改了一个属性,就可以有很大的性能提升。(这其实是反例,开启多了会更卡)

参考文章

没有排序,部分链接需要科学上网

Accelerated Rendering in Chrome
Scrolling Performance
Texture compression Wiki
A Reference Architecture for Web Browsers
GPU Accelerated Compositing in Chrome
On translate3d and layer creation hacks
GPU加速是什么
CSS GPU Animation: Doing It Right这样使用 GPU 渲染 CSS 动画
使用CSS3 will-change提高页面滚动、动画等渲染性能
CSS BEM 书写规范
关键渲染路径
css triggers
浏览器的工作原理:新式网络浏览器幕后揭秘
Webkit技术内幕
Web 性能优化-CSS3 硬件加速(GPU 加速)
理解WebKit和Chromium: 硬件加速之RenderLayer树到合成树

恰好你喜欢,恰好想喝咖啡