深入了解现代浏览器

271 阅读10分钟

一些基础概念

CPU - Central Processing Unit

CPU 即中央处理器,可以处理几乎所有计算。以前的 CPU 是单核的,现在大部分笔记电脑都是多核的,专业服务器甚至有高达 100 多核的。CPU 计算能力很强,但只能一件件事处理。

GPU - Graphics Processing Unit

GPU 一开始是为图像处理设计的,即主要处理像素点,所以拥有大量并行的处理简单事物的能力,非常适合用来做矩阵运算,而矩阵运算又是计算机图形学的基础,所以大量用在可视化领域。

CPU、GPU 都是计算机硬件,这些硬件各自都提供了一些接口供汇编语言调用;而操作系统则基于它们之上用 C 语言(如 linux)将硬件管理了起来,包括进程调度、内存分配、用户内核态切换等等;运行在操作系统之上的则是应用程序了,所以应用程序不直接和硬件打交道,而是通过操作系统间接操作硬件。显然,浏览器作为一个应用程序,运行在操作系统之上。

线程和进程

为了让程序运行的更安全,操作系统创造了进程与线程的概念,**进程(Process)可以分配独立的内存空间,进程内可以创建多个线程(Thread)**进行工作,这些线程共享内存空间。

因为线程间共享内存空间,因此不需通信就能交流,但内存地址相互隔离的进程间也有通信需求,需通过 **IPC(Inter Process Communication)**进行通信。

进程之间相互独立,即一个进程挂了不会影响到其它进程,而在一个进程中可以创建一个新进程,并与之通信,所以浏览器就采用了这种策略,将 UI、网络、渲染、插件、存储等模块进程独立,并且任意挂掉后都可以被重新唤起。

浏览器架构

浏览器是如何依靠进程/线程构建的呢?简单来说,就是一个进程同时创造了多个线程或进程(其中一些进程通过 IPC 通信)。

一个比较重要的点是,不同的架构只是实现细节上的差异。并没有所谓的规范来规定如何实现一个浏览器,不同浏览器的实现方式可能不尽相同。

以 Chrome 的架构举例,最顶层的是浏览器进程(Browser Process)跟其他负责不同部分的进程进行协调。其中部分进程和其所控制的功能如下:

  • Browser Process:控制应用的整体部分,包括地址栏、书签、前进后退按钮等,也负责浏览器不可见但优先级较高的部分比如网络请求、文件获取。

  • Renderer Process:控制 Tab 的展示部分(一般情况下每个 Tab 都有一个独立的渲染进程)。

  • Plugin Process:控制网站使用的插件部分。

  • GPU Process:相对隔离地处理 GPU 任务,这其中又可以分化为多个不同的进程,因为 GPU 从不同应用处理请求并把他们绘制在同一个界面上。

多进程架构的优劣

多进程架构的好处包括可以为每个 Tab 提供沙箱隔离,以提升稳定性(不会同时崩溃)并确保安全性。但多进程并不是毫无限制的,由于每个进程都会有一块独立的内存地址来存放相同的基础架构(例如 JavaScript 解析与执行引擎 V8),进程不能以线程的方式来共享存储。所以出于节约内存的角度考虑,Chrome 会限定进程个数的上线,这个限制取决于设备的内存情况和 CPU 性能,如果超出了这个限制,Chrome 就会使用一个进程来运行同一站点的不同 Tab。

Chrome 并不满足于采用一种架构,而是在不同环境下切换不同的架构。Chrome 将各功能模块化后,就可以自由决定当前将哪些模块放在一个进程中,将哪些模块启动独立进程,即可以在运行时决定采用哪套进程架构。这样做的好处是,可以在资源受限的机器上开启单进程模式,以尽量节约内存开销,实际上在手机应用上就是这么做的;而在资源丰富、内核数量充足的机器上采用独立进程模式,虽然消耗了更多资源,但获得了更好的稳定性。

Iframe 渲染进程 —— 站点隔离

site-isolation 将同一个 tab 内不同 iframe 包裹在不同的进程内运行,以确保 iframe 间资源的独占性,以及安全性。该功能直到 2018.7 才更新,是因为背后有许多复杂的工作要处理,比如开发者工具的调试、网页的全局搜索功能,都不能因为进程的隔离而受到影响,Chrome 必须让每个进程单独响应这些操作,并最终聚合在一起,让用户感受不到进程间的阻隔。

当你在地址栏中输入URL的时候发生了什么

1. 处理输入:UI Thread 响应用户输入,并且判断输入的字符串是查询还是 URL。

2. 开始导航:如果第一步输入的是合法网址,UI Thread 会初始化一个网络请求来获取站点信息。此时,Loading 圈圈会开始展示在标签页前面并且 Network Thread 会去寻找恰当的协议进行解析(例如 DNS)并且建立 TLS 连接。如果此时 Network Thread 收到了服务器返回的 301 之类的重定向头,则会通知 UI Thread 新建一个 URL 请求。

3. 解析响应内容:请求体返回时,Network Thread 会根据返回信息的前几个字段(即响应头)进行处理,例如 Content-Type 表明了返回数据的类型,若返回值缺失或错误,则会进行 MIME 类型嗅探

如果响应是一个 HTML 文件,那么下一步就会将数据交给渲染进程处理,但如果是 zip 或者其他文件就表明该请求是一个下载请求,会将数据交给下载器进行处理。

安全校验(SafeBrowsing)也会在这一步发生,如果域名或者返回值看起来匹配到已知的恶意站点,那么 Network Thread 就会提示并展示一个 warning 页面。

4. 寻找 Render Process:一旦相关检查完成并且 Network Thread 确认浏览器应该导航到对应站点,Network Thread 就会通知 UI Thread 数据 is Ready。UI Thread 就会实例化一个 Render Process 来承担页面渲染的任务。

因为网络请求的时长可能会达到几百毫秒,所以为了优化一些性能,UI Thread 通常会在第二步(此时 UI Thread 已经预先知道需要导航的站点)的时候就并行预先激活或者新建一个 Render Process。如果返回结果符合预期就会直接交给 Render Process 进行渲染,但如果检查失败则丢弃提前实例化好的 Render Process。

5. 提交导航:当返回数据和 Render Process 都准备好的时候,Browser Process 就会和 Render Process 通过 IPC 通信来确认提交导航,同时数据流也会被一并传递来确保 Render Process 可以持续接收 HTML 数据。一旦 Browser Process 确认 Render Process 已经发生提交了,导航过程就结束了并开始进行文档解析阶段。

此时,地址栏已经更新了,并且安全检查和站点 UI 设置都反映了新页面的信息。此 Tab 的会话记录也会被更新,这样当你点击前进/后退按钮的时候就能跳转到之前的页面,同时为了方便标签页关闭后快速恢复,会话记录会被存储在硬盘。

额外步骤:初始加载完成。当导航被提交之后,Render Process 就会携带着加载资源开始渲染页面。一旦 Render Process “完成” 了渲染,它就会通过 IPC 消息通知 Browser Process,此时 UI Thread 会让 Loading 转圈圈的图标消失。JS 脚本也有可能在这之后加载额外的远程资源并且进行渲染,但这都是加载状态完成之后的事了。

跳转到别的网站

当你准备跳转到别的网站时,Browser Process 在执行上述跳转流程前,还会响应当前 Render Process 的 beforeunload 事件(比如你可以使用 beforeunload 事件提示“是否要离开此页面”)。由于一个标签页内的所有东西(包括 JS 代码)都是由 Render Process 进行控制的,所以当一个新的导航请求进来的时候 Browser Process 需要跟 Render Process 进行检查。

(这里也提示了一下如非必要,不要添加额外的 beforeunload 处理,因为这一事件是在下一个导航开始前就执行的,可能会带来一些不必要的延迟)

如果导航是由 Render Process 来初始化的(比如用户点击了一个链接或者 JavaScript 运行了 window.location = "newsite.com" ),Render Process 就会去检查自己的 beforeunload 事件。然后将导航请求交由 Browser Process 来进行一遍上述的处理。

如果新的导航请求跟目前已经渲染的不是一个站点,那么就会重启一个 Render Process 来处理新的导航,同时旧的 Render Process 也会保留因为可能需要处理一些 unload 等事件。

(一个正在跳转的标签页中可能同时存在两个 Render Process)

如果存在 Service Worker

Service Worker 是在自己的应用层代码中进行网络代理的一种方式,它允许开发者能够对本地缓存和什么时候获取新数据有更多掌控权。如果 Service Worker 被设置为从缓存中加载页面,那么就不用从网络层获取数据。

一个比较关键的点是 Service Worker 也是一段 JavaScript 代码,所以它也是由 Render Process 来执行的。但是当新的导航请求发起的时候,Browser Process 是如何知道这个站点拥有 Service Worker 呢?

当一个 Service Worker 被注册的时候,它所相关的域名会被储存在一个列表中作为索引。所以当导航开始的时候,Network Thread 就会检查已经注册的 Service Worker 是否有这个站点。如果有的话,UI thread 就会找一个 Renderer Process 来执行相关的 Service Worker 代码。Service Worker 可能会从缓存中加载数据,也可能会通过网络发起新的请求。

导航预加载

如果 Service Worker 最终还是决定通过网络发起新的请求,那么整个在上述 Browser Process 和 Renderer Process 通信的流程中就可能会出现一些延迟。导航预加载就是一种可以加速这一进程的机制,它会在 Service Worker 启动之初并行加载资源。它会在这些请求的请求头中加一些标识符,允许服务端针对这些请求做特殊处理(比如只是更新一些数据而不是返回整个文档)。

思考

  • 多进程与少进程:资源和稳定性间的平衡

  • 模块解藕:Render Process 只关心渲染逻辑(修改应用状态、跳转链接)

  • 加速优化:资源换时间(提前创建 Renderer Process,提前发起 Network Process)

Refs

Inside look at modern web browser (part 1)

Inside look at modern web browser (part 2)