浏览器组成与渲染
下文都已Chrome为例。
浏览器的进程
进程
一个进程就是一个程序运行的实例。当一个程序启动是,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据,以及执行任务的主线程,我们把这样的一个运行环境称为进程。
当一个进程关闭之后,操作系统会回收进程所占用的内存空间。
进程之间的内容相互隔离,但是可以使用 IPC机制 进行通信
线程
线程不能单独存在,使用进程来启动个管理的。一个进程中可以存在多个线程。
线程之间共享进程中的数据。
进程中任意线程执行出错,都会导致整个进程的崩溃
单进程的浏览器时代
单进程浏览器是指浏览器所有的功能模块都运行在一个进程之内,带来的问题是 不稳定、不流畅、不安全
总体来说,单进程浏览器,更容易导致浏览器崩溃,卡顿。如插件崩溃,内存泄漏,恶意插件攻击等问题。
早期多进程浏览器时代
基于单进程浏览器所带来的诸多问题,多进程浏览器应运而生。
拆分出 渲染进程、插件进程、浏览器主进程。
每个页面都有一个渲染进程,这样即使某一个页面出现崩溃(内存泄漏或 JS执行异常)都不会影响到其他渲染进程。且当前页面关闭之后,渲染进程也随之关闭,内存泄漏的问题也解决了。
至于安全问题,多进程另外一个好处就是可以使用**安全沙箱**。
不允许读写硬盘上的数据,不允许获取操作系统权限。这样即使有恶意程序执行,也只是在渲染进程或插件进程内执行。无法获取系统权限,从而大大提高安全性
当前多进程架构
拆分为 浏览器主进程、渲染进程、插件进程、网络进程、GPU进程、存储进程、跟踪服务进程
浏览器的进程模式
Chrome浏览器提供四种进程模式。
Process-per-site-instance(default):同一个site-instance使用一个进程
Process-per-site:同一个 site 使用一个进程
Process-per-tab:每个 tab 使用一个进程
Single process:所有tab公用一个进程
解释下 Process-per-site 和 Process-per-site-instance
Process-per-site:指相同域名,共用同一个进程。例如 a.baidu.com和b.baidu.com。也就是用,其中一个页面崩溃,另一个页面也随之崩溃。
Process-per-site-instance:当打开一个tab访问a.baidu.com,再打开一个tab访问b.baidu.com,此时两个tab是两个进程。如果在a.baidu.com中,通过js代码打开了b.baidu.com,这两个tab会使用统一个进程。
为什么Chrome会选择Process-per-site-instance作为默认模式:
- 相比于Process-per-tab,少开很多进程,节省内存
- 相比于Process-per-site,能够更好的隔离相同域名下毫无关联的tab,更加安全
总结
浏览器的进程架构还在不断演进,当前是SOA架构(Services Oriented Architecture:面向服务的体系结构)
最终会把UI、数据库、网络、文件、设备、媒体、图形库等变成基础服务,类似于操作系统底层服务。
同时,Chrome还提供灵活的弹性架构,在性能强大的设备上会运行更多的基础进程服务,在资源受限的设备上,Chrome会将多个服务合并到一个进程中,从而节省内存。
网页的加载过程
Tab以外的大部分工作有浏览器进程负责,针对工作的不同,浏览器进程划分出不同的工作线程。
UI thread:控制浏览器上的按钮及输入框
network 进程:处理网络请求
storage 进程:控制文件等的访问
输入过程
地址栏输入URL或关键字,UI Thread会判断是url或是关键字
如果是关键字,UI线程会使用浏览器默认的搜索引擎来,合成新的带关键字的URL
如果是符合URL规则,把这段内容加上协议,合成完成的URL
请求过程
回车按下后,UI线程将URL通过IPC发送给网络进程,网络进程发起真正的请求。此时UI线程将Tab前的图标变成加载中状态。
网络进程在真正发起请求之前,会检查一下本地是否缓存了当前资源,如果缓存,直接返回给浏览器进程。
如果没有找到该资源,则进入网络请求流程。
首先进行DNS解析,获取请求服务器的IP,如果是HTTPS协议,则建立TLS连接。
建立TCP连接(三次握手),构建请求头信息,发送HTTP请求。
服务端接收请求后,程序处理,构建响应头,发送给网络进程。网络进程接收到响应信息,开始解析。
重定向
如果发现返回的状态码是301或302,网络进程会从响应头的Location字段里读取重定向的地址,在发起新的HTTP请求。
响应数据类型
URL请求的数据类型,有时候回事一个下载类型,有时候是一个正常的HTML页面。
如何进行区分呢?
可以通过Content-Type进行区分。如果是application/octet-stream,则是字节流格式的数据,就是下载类型。此时,会将数据交给浏览器的下载管理器处理。
如果响应头中字段的值为 text/html,则返回的数据是html格式。则会交给浏览器的渲染引擎去处理。
安全检查
在发送请求的过程中,网络进程会进行安全检查(Safe Browsing)。如果域名或者请求内容匹配到已知的恶意站点,网络进程会展示一个警告页。
准备渲染进程
这一步和网络进程发起请求请求是并行的。响应数据准备好之后,渲染进程其实也早就准备好了。
这里会涉及到上面所提到的浏览器的进程模式。如果新打开的tab页面,浏览器会分配一个独立的渲染进程。如果在A页面打开B页面,且AB都属于同一站点,则AB共用一个渲染进程。
当渲染进程准备好之后,还不能马上进行渲染,因为数据还在网络进程内,下一步就是将数据传递给渲染进程。
提交文档
当网络进程准备好数据之后,会通知浏览器进程(IPC)。
浏览器进程接收到通知,会向渲染进程发出【提交文档】的消息(IPC)。
渲染进程接收到消息之后,会和网络进程建立数据传输的“管道”(IPC)。
当文档数据传输完成之后,渲染进程会返回【确认提交】的消息给浏览器进程(IPC)。
浏览器进程收到消息之后,会更新浏览器界面状态。
- 更新前进后退的状态
- 更新安全指示符
- 更新地址栏URL
至此,一个完整的导航流程结束,之后就会进入渲染流程。
渲染阶段
渲染进程中,主要包括以下线程:
- 一个主线程
- 多个工作线程
- 一个合成器线程
- 多个光栅化线程
不同的线程,有着不同的工作职责:
构建DOM树
为什么要构建dom树呢,因为浏览器不能理解HTML,但是能理解DOM。
主线程解析HTML数据,主要利用HTML解析器,从上到下逐行解析。
这个过程为【字节 → 字符 → 令牌 → 节点】
子资源加载
当遇到CSS、图片、视频、Script、ifrem等资源,渲染进程会和网络进程进行IPC通信。网络进程会发起请求获取资源。为了提升效率,浏览器也会运行预加载扫描程序,如果HTML中包含外部资源,浏览器进程会调用网络进程去进行资源获取。
html的解析可以与css的解析并行
JS的下载与执行
在构建DOM的过程中,如果遇到
JavaScript 需要查询 CSS 信息,所以 JavaScript 还要等待 CSSOM 树构建完才可以执行
可以在
- 两者都不会阻止 document 的解析
defer
会在 DOMContentLoaded 前依次执行async
则是下载完立即执行,不一定是在 DOMContentLoaded 前async
因为乱序,所以很适合无依赖脚本
样式计算
DOM树只是页面的结构。但是我们还需要每一个DOM节点的样式。
样式来源:行内样式、style标签、link引入、浏览器内置样式
-
CSS解析器会将css转化为StyleSheets。
-
属性值标准化。比如css的属性值可能有
color: red; font-size: 2rem width: 50%
css解析器需要将以上同一类属性值统一成一种单位。
-
利用css的继承规则、层叠规则,计算出每个节点的样式。
布局
通过DOM解析和样式计算,我们已经获取到带有样式的DOM树(Layout Tree)。但是我们还需要知道每一个DOM节点在页面上的位置。
主线程会遍历DOM树。创建布局树:
- 遍历DOM树中的所有可见节点,并把这些节点添加到布局中。
- 不可见元素包含:display:none、head标签/内部子标签、script标签
布局计算:太过复杂,涉及到浏览器内核的算法。当前只知道有这么个事。
分层
由于页面中有很多复杂的效果,比如3D动画、Z轴排序,页面轮播等,所以渲染引擎还需要为特定的节点生成专用的图层。且生成一个图层树(LayerTree)
并不是每个节点都包含一个图层,如果一个节点没有对应的图层,那么它就从属于父图层。
分层标准:
- 拥有层叠上下文属性的节点会被提升为单独一层(position+z-index、opactity、filter等)
- 需要被裁减的元素也回被提升为单独一层(文字超出显示区域,配合overflow:hidden)
绘制
分层之后,我们能拿到的全部资源包括:
元素之间的父子关系、元素的样式、元素在页面中的几何位置、元素图层层级关系
除以上这些之外,我们还需要知道每个元素绘制的先后顺序。
渲染引擎会把一个图层拆分成很多小的绘制指令,然后再把这些指令按照绘制顺序组成绘制列表。
在图层绘制阶段,主要就是输出一个个的待绘制列表。
合成
有了绘制列表,就可以绘制一个页面了。主线程会把这些信息通知给合成线程。合成线程会将图层划分为图块。图块的大小通常是256x256或者512x512
将图块转换为位图,这个过程叫光栅化操作。
早期Chrome采用只光栅化视口内的网页内容,如果用户滑动页面,就会移动光栅帧,且需要光栅化新的内容不足页面上的缺失。那么每当页面滚动都去进行光栅化,非常损耗性能。Chrome进行优化,叫做合成。
渲染进程维护了一个光栅化的线程池。图块是光栅化执行的最小单位。所有的图块栅格化都是在线程池内执行的。
光栅化过程都会使用 GPU 来加速生成,由渲染进程和GPU进程进行通信。GPU生成的位图就保存在GPU内存当中。
为了优化显示体验,合成线程可以给不同的光栅线程赋予不同的优先级,将那些在视口中的或者视口附近的层先被光栅化。
当图层上所有的图块都被光栅化完毕,合成线程会收集图块上面叫做DrawQuad(绘画四边形)命令来构建合成帧。
- 绘画四边形:包含图块在内存的位置以及图层合成后图块在页面的位置之类的信息。
- 合成帧:代表页面一个帧的内容的绘制四边形集合。
以上步骤完成之后,合成线程通过IPC向浏览器进程提交一个渲染帧。
浏览器进程收到信息之后,调用GPU进程,将合成帧的对应图块显示在屏幕上。
合成线程收到页面滚动的事件,合成线程会构建另外一个合成帧发送给GPU来更新页面。
合成的好处:
这个过程没有涉及到主线程,所以合成线程不需要等待样式的计算以及JavaScript完成执行。这就是为什么合成器相关的动画最流畅,如果某个动画涉及到布局或者绘制的调整,就会涉及到主线程的重新计算,自然会慢很多。
浏览器事件处理
输入事件
合成器线程接受输入事件。合成线程会将页面里添加了事件监听区域标记为 非立即可滚动区。
如果事件发生在这个区域,合成线程可以去顶应将其发往主线程。
如果事件发生在该区域外,合成线程无需等待主线程,可继续合成新帧。
/*
这种写法等于把整个页面都标记成了 非立即可滚动区
合成线程必须在每次输入事件产生后与主线程通信并等待返回
会导致合成线程等待,浪费时间
*/
document.body.addEventListener('touchstart', (event) => {
if (event.target === area) {
event.preventDefault();
}
});
/*
添加一个 passive: true 选项 ,将这种负面效果最小化
提示浏览器你想继续在主线程中监听事件,但合成器不必停滞等候,可接着创建新的合成帧。
*/
document.body.addEventListener(
'touchstart',
(event) => {
if (event.target === area) {
event.preventDefault();
}
},
{
passive: true,
}
);
定位事件目标
合成器将输入事件发送至主线程之后,首先运行的是命中检测。
命中检测会使用绘制记录数据,找出事件发生坐标下的内容。