[TOC]

CPU 与 GPU

CPU 和 GPU 作为计算机中最重要的两个计算单元直接决定了计算性能。

CPU

Chrome的架构 - 图1
CPU.png
CPU 是计算机的大脑,负责处理各种不同的任务。在过去,大多数 CPU 是单芯片的,核心被安置在同一个芯片上。更新的 CPU 可以支持多核心,运算能力大大加强。而最新的的 cpu 已经达到 10 核心 20 线程数的能力了。

GPU

Chrome的架构 - 图2
GPU.png
GPU 是另一个计算机的组成部分,与 CPU 不同,GPU 更擅长利用多核心同时处理单一的任务。像命名那样,GPU 最初被用于处理图像。这就是为什么使用 GPU 可以更快、更顺畅的渲染页面内容。随着 GPU 的发展,越来越多的计算任务也可以使用 GPU 来处理。甚至有人说 GPU 是人工智能的大功臣,可见 GPU 已经不再仅用于图像处理上了。

计算机架构

Chrome的架构 - 图3
hw-os-app.png
我们可以把计算机自下而上分成三层:硬件、操作系统和应用。有了操作系统的存在,上层运行的应用可以使用操作系统提供的能力使用硬件资源而不会直接访问硬件资源。

进程与线程

Chrome的架构 - 图4
process-thread.png
一个进程是应用正在运行的程序。而线程是进程中更小的一部分。当应用被启动,进程就被创建出来。程序可以创建线程来帮助其工作。操作系统会为进程分配私有的内存空间以供使用,当关闭程序时,这段私有的内存也会被释放。其实还有比线程更小的存在就是协程,而协成是运行在线程中更小的单位。async/await 就是基于协程实现的。

进程间通信(IPC)

Chrome的架构 - 图5
一个进程可以让操作系统开启另一个进程处理不同的任务。当两个进程需要通信时,可以时用 IPC(Inter Process Communication)。
多数程序被设计成使用 IPC 来进行进程间的通信,好处在于当一个进程给另一个进程发消息而没有回应时,并不影响当前的进程继续工作。

浏览器架构

借助进程和线程,浏览器可以被设计成单进程、多线程架构,或者利用 IPC 实现多进程、多线程架构。
Chrome的架构 - 图6
browser-arch.png
这里我们以 Chrome 多进程架构介绍,在 Chrome 中存在这不同种类型的进程,它们各司其职。
Chrome的架构 - 图7
browser-arch2.png
浏览器进程做为 Chrome 中最核心的进程管理着 Chrome 中的其他进程,而 Renderer 则负责渲染不同的站点。

进程工作内容

Chrome的架构 - 图8
browserui.png

浏览器进程(Browser process)

浏览器进程负责管理 Chrome 应用本身,包括地址栏、书签、前进和后退按钮。同时也负责可不见的功能,比如网络请求、文件按访问等,也负责其他进程的调度。

渲染进程(Renderer process)

渲染进程负责站点的渲染,其中也包括 JavaScript 代码的运行,web worker 的管理等。

插件进程(Plugin process)

插件进程负责为浏览器提供各种额外的插件功能,例如 flash。

GPU 进程(GPU process)

GPU 进程负责提供成像的功能。
当然还有其他像扩展进程或工具进程等其他进程,可以在 Chrome 的 Task Manager 面板中查看,面板中列出了运行的进程和其占用的 CPU、内存情况。

多进程架构的好处

当我们访问一个站点时,渲染进程会负责运行站点的代码,渲染站点的页面,同时响应用户的交互动作,当我们在 Chrome 中打开三个页签同时访问三个站点时,如果其中一个没有响应,我们可以关闭它然后使用其他的页签,这是因为 Chrome 为每个站点创建一个独立的渲染进程,专门处理当前站点的渲染工作。如果所有的页面运行在同一个进程中,当有一个页面没有响应时,所有的页面就都卡住了。
Chrome的架构 - 图9
tabs.png
另一个好处是,借助操作系统对进程安全的控制,浏览器可以将页面放置在沙箱中,站点的代码可以运行在隔离的环境中,保证核心进程的安全。
虽然多进程的架构优于单进程架构,但由于进程独享自己的私有内存,以渲染进程为例,虽然渲染的站点不同,但工作内容大体相似,为了完成渲染工作它们会在自己的内存中包含相同的功能,例如 V8 引擎(用于解析和运行 Javascript),这意味着这部分相同的功能需要占用每个进程的内存空间。为了节省内存,Chrome 限制了最大进程数,最大进程数取决于硬件的能力,同时当使用多个页签访问相同的站点时浏览器不会创建新的渲染进程

面向服务的架构

Chrome 将架构从多进程模型转变成面向服务。浏览器将功能以服务的方式提供,以解决多进程架构中的问题。
当 Chrome 运行在拥有强大硬件的计算机上时,会将一个服务以多个进程的方式实现,提高稳定性,当计算机硬件资源紧张时,则可以将多个服务放在一个进程中节省资源。
Chrome的架构 - 图10
servicfication.png

基于站点隔离的渲染进程

利用 iframe 我们可以在同一个页面访问不同站点的资源,但从安全的角度考虑,同源策略不允许一个站点在未得到同意的情况下访问其他站点的资源,所以从 Chrome 67 开始每个站点由独立的渲染进程处理被默认启用。
Chrome的架构 - 图11
isolation.png

浏览器进程

Chrome的架构 - 图12
browserprocesses.png
浏览器进程负责处理除了渲染外的大部分工作,浏览器进程包括几个线程:

  • UI 线程负责绘制工具栏中的按钮、地址栏等。
  • 网络线程负责从网络中获取数据。
  • 存储线程负责文件等功能。

当我们在地址栏中输入一个地址时,浏览器进程中的 UI 线程最先得知这个动作,并开始处理。

一次访问

下面我们就从一次常见的访问入手,逐步了解浏览器是如何展示页面的。

Step 1:输入处理

当我们在地址栏中输入时,UI 线程会先判断我们输入的内容是要搜索的内容还是要访问一个站点,因为地址栏同时也是一个搜索框。
Chrome的架构 - 图13
input.png

Step 2:访问开始

当我们按下回车开始访问时,UI 线程将借助网络线程访问站点资源. 浏览器页签的标题上会出现加载中的图标,同时网络线程会根据适当的网络协议,例如 DNS lookup 和 TLS 为这次请求建立连接。
Chrome的架构 - 图14
navstart.png
当服务器返回给浏览器重定向请求时,网络线程会通知 UI 线程需要重定向,然后会以新的地址做开始请求资源。

Step 3:处理响应数据

Chrome的架构 - 图15
response.png
当网络线程收到来自服务器的数据时,会试图从数据中的前面的一些字节中得到数据的类型(Content-Type),以试图了解数据的格式。
当返回的数据类型是 HTML 时,会将数据传递给渲染进程做进一步的渲染工作。但是如果数据类型是 zip 文件或者其他文件格式时,会将数据传递给下载管理器做进一步的文件预览或者下载工作。
Chrome的架构 - 图16
sniff.png
在开始渲染之前,网络线程要先检查数据的安全性,这里也是浏览器保证安全的地方。如果返回的数据来自一些恶意的站点,网络线程会显示警告的页面。同时,Cross Origin Read Blocking(CORB)策略也会确保跨域的敏感数据不会被传递给渲染进程。

Step 3:渲染过程

当所有的检查结束后,网络线程确信浏览器可以访问站点时,网络线程通知 UI 线程数据已经准备好了。UI 线程会根据当前的站点找到一个渲染进程完成接下来的渲染工作。
Chrome的架构 - 图17
findrenderer.png
在第二步,UI 线程将请求地址传递给网络线程时,UI 线程就已经知道了要访问的站点。此时 UI 线程就可以开始查找或启动一个渲染进程,这个动作与让网络线程下载数据是同时的。如果网络线程按照预期获取到数据,则渲染进程就已经可以开始渲染了,这个动作减少了从网络线程开始请求数据到渲染进程可以开始渲染页面的时间。当然,如果出现重定向的请求时,提前初始化的渲染进程可能就不会被使用了,但相比正常访问站点的场景,重定向往往是少数,在实际工作中,也需要根据特定的场景给出特定的方案,不必追求完美的方案。

Step 4:提交访问

经历前面的步骤,数据和渲染进程都已经准备好了。浏览器进程会通过 IPC 向渲染进程提交这次访问,同时也会保证渲染进程可以通过网络线程继续获取数据。一旦浏览器进程收到来自渲染进程的确认完毕的消息,就意味着访问的过程结束了,文档渲染的过程就开始了。
这时,地址栏显示出表明安全的图标,同时显示出站点的信息。访问历史中也会加入当前的站点信息。为了能恢复访问历史信息,当页签或窗口被关闭时,访问历史的信息会被存储在硬盘中。
Chrome的架构 - 图18
commit.png

Extra Step:加载完毕

当访问被提交给渲染进程,渲染进程会继续加载页面资源并且渲染页面。当渲染进程”结束”渲染工作,会给浏览器进程发送消息,这个消息会在页面中所有子页面(frame)结束加载后发出,也就是 onLoad 事件触发后发送。当收到”结束”消息后,UI 线程会隐藏页签标题上的加载状态图标,表明页面加载完毕。
但这里”结束”并不意味着所有的加载工作都结束了,因为可能还有 JavaScript 在加载额外的资源或者渲染新的视图。
Chrome的架构 - 图19
loaded.png

访问不同的站点

一次普通的访问到此就结束了。当我们输入另外一个地址时,浏览器进程会重复上面的过程。但是在开始新的访问前,会确认当前的站点是否关心beforeunload事件。
beforeunload事件可以提醒用户是否要访问新的站点或者关闭页签,如果用户拒绝则新的访问或关闭会被阻止。
由于所有的包括渲染、运行 Javascript 的工作都发生在渲染进程中,浏览器进程需要在新的访问开始前与渲染进程确认当前的站点是否关心unload。
Chrome的架构 - 图20
beforeunload.png
如果一次访问是从一个渲染进程中发起的,例如用户点击一个链接或者运行 JavaScript 代码location = ‘http://newsite.com'时,渲染进程首先检查beforeunload。然后再执行和浏览器进程初始化访问同样的步骤,只不过区别在于这样的访问请求是由渲染进程向浏览器进程发起的。
当新的站点请求被创建时,一个独立的渲染进程将被用于处理这个请求。为了支持像unload的事件触发,老的渲染进程需要保持住当前的状态。更详细的生命周期介绍可以参考Page lifecycle
Chrome的架构 - 图21
unload.png

Service worker

Service worker 是一种可以 web 开发者控制缓存的技术。如果 Service worker 被实现成从本地存储获取数据时,那么原本的请求就不会被浏览器发送给服务器了。
值得注意的是,Service worker 中的代码是运行在渲染进程中的。当访问开始时,网络线程会根据域名检查是否有 Service worker 会处理当前地址的请求,如果有,则 UI 线程会找到对应的渲染进程去执行 Service worker 的代码,而 Service worker 可以让开发者决定这个请求是从本地存储还是从网络中获取数据。
Chrome的架构 - 图22
scope_lookup.png
Chrome的架构 - 图23
serviceworker.png

访问预加载

如果 Service worker 最终决定要从网络中获取数据时,我们会发现这种跨进程的通信会造成一些延迟。Navigation Preload是一种可以在 Service worker 启动的同时加载资源的优化机制。借助特殊的请求头,服务器可以决定返回什么样的内容给浏览器。
Chrome的架构 - 图24
navpreload.png

渲染进程负责页面的内容

渲染进程负责所有发生在浏览器页签中的事情。在一个渲染进程中,主线程负责解析,编译或运行代码等工作,当我们使用 Worker 时,Worker 线程会负责运行一部分代码。合成线程和光栅线程是也是运行在渲染进程中的,负责更高效和顺畅的渲染页面。
渲染进程最重要的工作就是将 HTML、CSS 和 Javascript 代码转换成一个可以与用户产生交互的页面。
Chrome的架构 - 图25
renderer.png

解析过程

下面的章节主要介绍渲染进程如何将从网络线程中获取的文本转化成图像的过程。

DOM 的创建

当渲染进程接收到来自浏览器进程提交访问的消息后就开始接受 HTML 数据,主线程开始解析 HTML 文本字符串,并且将其转化成 Document Object Model(DOM)
DOM 是一种浏览器内部用于表达页面结构的数据,同时也为 Web 开发者提供了操作页面元素的接口,让 web 开发者可以在 Javascript 代码中获取和操作页面中的元素。
将 HTML 文本转化成 DOM 的标准被HTML Standard定义。我们会发现在转化过程中浏览器从来不会抛出异常,类似关闭标签的丢失,开始、关闭标签匹配错误等等。这是因为 HTML 标准中定义了要静默的处理这些错误,如果对此感兴趣可以阅读An introduction to error handling and strange cases in the parser

额外资源的加载

一个网站通常还会使用类似图片,样式文件和 JavaScript 代码等额外的资源。这些资源也需要从网络或缓存中获取。主线程在转化 HTML 的过程中理应挨个加载它们,但是为了提高效率,预加载扫描(Preload Scanner)与转换过程会同时运行着。当预加载扫描在分析器分析 HTML 过程中发现了类似 img 或 link 这样的标签时,就会发送请求给浏览器进程的网络线程,而主线程会根据这些额外资源是否会阻塞转化过程而决定是否等待资源加载完毕。
Chrome的架构 - 图26
dom.png

JavaScript 会阻塞转化过程

当 HTML 分析器发现