经历了DNS查询 -> TCP连接 -> HTTP请求 -> 服务器响应 -> 浏览器解析HTML,构建DOM树 -> 解析CSS,生成CSS规则树 -> 合并两颗树,生成render tree -> 布局render树,负责各元素尺寸、位置的计算 -> 绘制render树,绘制页面像素信息 -> 浏览器将各层的信息发送给GPU, GPU会将各层合成,显示在屏幕上

1. 多进程的浏览器

浏览器是多进程的,包括主控进程,插件进程,GPU,每一个tab页都会新开一个进程(某些情况下多个tab会合并进程)。

  • Browser进程: 浏览器的主进程(负责协调、主控),只有一个
  • 第三方插件进程: 每个类型的插件对应一个进程,仅当使用该插件时才创建
  • GUP进程: 最多一个,用于3D绘制
  • 浏览器渲染进程(内核): 默认每个tab页面都会新开一个进程,互不影响,控制页面渲染,脚本执行,事件处理等。(有时会优化,如多个空白tab会合并成一个进程)。


需要注意,浏览器和浏览器内核是不同的概念,浏览器指的是Chrome, Firefox,而浏览器内核是Blink, Gecko, 浏览器内核只负责渲染,GUI及网络连接等跨平台工作则是浏览器实现的。

2. 浏览器渲染进程(内核)

每新建一个tab页面会新开一个浏览器渲染进程,这个进程是多线程的,用来处理页面的渲染,JS的执行,事件的循环。

2.1 子线程

浏览器渲染进程,有几大类子线程:

2.1.1 GUI渲染线程
  • 负责渲染浏览器界面,解析HTML, CSS, 构建DOM树和RenderObject树,布局和绘制等。
  • 当界面需要重绘(Repaint) 或由于某种操作引发回流(reflow)时,该线程就会执行
  • 注意,GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

(由于Javascript可以操纵DOM,如果修改这些元素属性同时渲染页面,那么渲染线程前后获得的元素数据就可能不一致,为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS互斥的关系。)

2.1.2 JS引擎线程
  • 也成为JS内核,负责处理Javascript脚本程序(如V8引擎)
  • JS引擎线程负责解析Javascript脚本,运行代码
  • JS引擎一直等待着任务队列中任务的到来,然后加以处理。
  • GUI渲染线程与JS引擎线程是互斥的,如果JS执行的时间过长,就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
2.1.3 事件触发线程
  • 当JS引擎执行代码块如setTimeOut,绑定鼠标点击,ajax异步请求,会将对应任务添加到事件线程中。
  • 当对应的事件触发条件被触发时,该线程会把时间添加到待处理队列的队尾,等待JS引擎的处理
  • 由于JS的单线程关系,这些待处理队列中的事件,只在JS引擎空闲时才会去执行
2.1.4 定时触发器线程
  • setIntervalsetTimeout所在线程
  • 浏览器定时计数器不是由JS引擎计数的(因为Javascript引擎是单线程的,如果处于阻塞线程状态,计时将不准确)
  • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲时执行)
2.1.5 异步http请求线程
  • 发起XMLHttpRequest连接后,浏览器会新开一个线程处理请求
    (一个http一个线程?)
  • 在检测到状态变更时,如果设置有回调函数,异步线程就会产生状态变更事件,将这个回调再放入事件队列中。由JS引擎执行。

2.2 常见浏览器内核

浏览器渲染进程,指的就是我们平时所说的浏览器内核。浏览器内核可以分为两部分: 渲染引擎和JS引擎。渲染引擎负责取得网页的内容(HTML, XML, 图像等等)、整理讯息(加入CSS), 以及计算网页的显示方式,然后输出至显示器或打印机。JS引擎,则是解析和执行javascript。

最开始渲染引擎和JS引擎没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎。常见的浏览器内核:Trident(IE内核),

1. Trident(IE内核)
IE内核在1997年的IE4中首次被采用,是微软在Mosaic(人类第一个浏览器)代码基础上修改而来的,并沿用到IE11。IE内核曾经几乎与W3C标准脱节(2005年),内核中大量bug等安全性问题也没有得到及时解决。国内很多双核浏览器的其中一核便是Trident,美其名曰“兼容模式”。Window10发布后,IE将其内置浏览器命名为Edge, Edge最显著的特点就是新内核EdgeHTML。

2. Gecko(Firefox)
Netscape6, Mozilla Firefox也采用了该内核。Gecko引擎与IE不无关系,IE没有使用W3C标准,导致了微软内部一些开发人员的不满,他们与当时已经停止更新了的Netscape的一些员工,一起创办了Mozilla。

3. Webkit(Safari)
当年苹果在比较了Gecko和KHTML后,选择了后者来做引擎开发,是因为KHTML拥有清晰的源码结构和极快的渲染速度。Webkit内核 可以说是以硬件盈利为主的苹果公司给软件行业的最大贡献之一。随后,2008年谷歌公司发布Chorme浏览器,采用的chromium内核就是fork了Webkit

4. Chromium/Blink(Chrome)
2008年,谷歌公司发布了chrome浏览器,浏览器使用的内核被命名为chromium。choromium fork字开源引擎webkit。谷歌公司还研发了自己的javascript引擎,V8,极大地提高了javascript的运算速度。chromium问世后,一些基于chromium的单核,双核浏览器拔地而起,如搜狗、360、qq浏览器。2013年,谷歌发表将于webkit分道扬镳,在chromium项目中研发Blink渲染引擎,内置于Chorme浏览器中。

5. 移动端内置浏览器内核
目前移动设备浏览器上常用的内核有Webkit, Blink, Trident, Gecko等。其中iphone和iPad等ios平台主要是webkit, Android 4.4之前安卓系统浏览器内核是webkit, android系统浏览器切换到chromium。Window Phone 8系统浏览器内核是Trident。

3. 解析URL

当操作系统GUI将输入事件传递到了浏览器,在这过程中,浏览器可能会做一些预处理,比如Chrome会根据历史统计来预估输入字符对应的网站。比如输入了[ba],根据之前的历史发现90%的概率会访问[www.baidu.com],因此就会在输入回车前开始建立TCP链接甚至渲染了。

输入URL后的[回车],这时浏览器会进行检查,首先判断协议,如果是http就按照web来处理,调用浏览器内核中的对应方法,如WebView中的loadUrl方法。从应用角度看,主要做两件事件:通过DNS查询IP,通过Socket发送数据。每个网络请求时都需要开辟单独的线程进行。

4. 浏览器渲染页面

从耗时的角度,浏览器请求、加载、渲染一个页面,时间花在下面五件事件上:

  • DNS查询
  • TCP连接
  • HTTP请求
  • 服务器响应
  • 客户端渲染

浏览器对内容的渲染(渲染树构建、布局及绘制),又可以分为下面几个步骤:

  • 解析HTML,构建DOM树
  • 解析CSS,生成CSS规则树
  • 合并DOM树和CSS规则树,生成render树
  • 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  • 绘制render树(paint),绘制页面像素信息
  • 浏览器会将各层的信息发送给GPU, GPU会将各层合成(composite),显示在屏幕上

如果DOM或CSSDOM被修改,上面的过程需要重复执行,才能计算出哪些像素需要在屏幕上重新渲染。实际上,CSS与javascript往往会多次修改DOM和CSSDOM。

4.1 DOM树

假设拿到了这样的页面:

1
2
3
4
5
6
7
8
9
10
11
<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,构建出DOM的过程可以简述如下:

1
Bytes → characters → tokens → nodes → DOM

转换:浏览器先将获得的HTML内容(Bytes)基于指定的编码转换为单个字符。
分词:按照HTML规范将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集。
词法分析:分词的结果是得到一堆的token,此时把他们转换为对象,这些对象分别有它们对应的属性和规则
DOM构建:根据标签之间的贵溪,最终得到一个树形结构的DOM树。

4.2 CSS规则树

CSS规则树的生成也是类似

1
Bytes -> characters -> tokens -> nodes -> CSSDOM

4.3 Render树

当DOM树和CSSDOM树都有了后,就要构建渲染树了。一般来说,渲染树和DOM树相对应,但不是严格意义上的一一对应,因为有一些不可见的DOM元素不会插入到渲染树中。如display:none的元素,和<head>这样的不可见标签。

4.4 渲染

有了render树,接下来就是渲染,主要有4个步骤:

  • 计算CSS样式
  • 构建渲染树
  • 布局,主要定位坐标的大小,是否换行,各种position overflow z-index属性
  • 绘制,将图像绘制出来
    1
    Render Tree -> Compute style -> construct frames -> layout -> paint

js动态修改dom或css,会导致重新布局(Layout)或渲染(Repaint)。

4.4.1 回流

Layout,也称为Reflow, 回流。意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树。

  • 页面渲染初始化
  • DOM结构改变,如删除了某个几点
  • render树变化,如减少了padding
  • 窗口resize
  • 获取某些属性也会触发回流:offset(Top/Left/Width/Height), scroll(Top/Left/Width/Height), client(Top/Left/Width/Height), width, height, 调用getComputedStyle
4.4.2 重绘

Repaint,即重绘。意味着只有一些外观属性发生了变化(例如,背景色,边框颜色,文字颜色),此时只需要应用新样式绘制这个元素就可以了。

1
2
3
4
5
6
7
8
//
var s = document.body.style;
s.padding = "2px"; // 回流+ 重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 再一次 重绘
s.backgroundColor = "#ccc"; // 再一次 重绘
s.fontSize = "14px"; // 再一次 回流+重绘
document.body.appendChld(document.createTextNode('abc!')); // 再一次 回流+重绘
4.4.3 避免回流

回流的成本开销要高于重绘,一个节点的回流往往导致子节点以及同级节点的回流,尽量避免回流一般有这些优化方案:

  • 减少逐项更改样式,一次性修改style,或将样式已定义为class一次性更新
  • 避免循环操作dom,创建一个documentFragment或div,在上面应用所有dom后,再添加到window.document
  • 避免多次读取offset属性,缓存到变量
  • 复杂的元素用绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高

4.5 资源外链下载

当html解析器被脚本阻塞时,解析器虽然会停止构建DOM,但仍会识别该脚本后面的资源,并进行预加载。它们包括CSS样式资源,JS脚本资源,img图片资源。遇到这些外链时,会单独开启一个下载线程去下载资源。

css样式资源

  • css下载时异步,不会阻塞浏览器构建dom树
  • css被视为阻塞渲染的资源,浏览器将不会渲染任何已处理的内容,知道CSSDOM构建完毕
  • media query声明的css不会阻塞渲染,会在符合条件时阻塞渲染

js脚本资源

  • 当浏览器遇到一个script标签时,DOM构建将暂停,直至脚本完成执行
  • 在脚本阻塞时,也会继续下载其他资源,但解析过程仍然是阻塞的
  • defer: 延迟执行引入的js,即这段js加载时html并未停止解析,这两个过程是并行的。整个document解析完毕而且defer-script也加载完成之后,会执行所有由defer-script加载的js代码,然后触发DOMContentLoaded事件。
  • async: 异步执行引入的js,async-script可能在DOMContentLoaded触发之前或之后执行,但一定在load触发之前执行。多个async-script的执行顺序是不确定的。注意:向document动态添加script标签时,async属性默认是true。如果想同步执行,需要把async属性人为设置为false。
    1
    2
    <script src="app.js" defer></script>
    <script src="app.js" async></script>

img图片资源
遇到图片资源时,直接会异步下载,不会阻塞解析,下载完后直接用图片替换原有src的地方

参考资料