一次导航到底发生了什么?

console.info

本文翻译自 Google 的官方文档,该系列共 4 篇文章,从内部观察现代浏览器 (Chrome),同时解答了浏览器的内部架构,讲述了浏览器从输入 url 到页面呈现的全过程。

原文链接: inside-browser-part2


前言

这是 从内部观察现代浏览器 (Chrome) 系列文章的第二篇,在上一篇文章中,我们了解了浏览器的组成,以及如何将任务分配到不同的进程 & 线程。在这篇文章中,我们将深入了解:为了将一个 web 页面呈现到浏览器上,各各不同的进程 & 线程是如何进行协作的。

浏览器都会有这个简单的场景:用户在导航栏里输入 url ,接着浏览器到对应的服务器获取数据并将页面内容呈现到浏览器上。在这篇文章中,我们将了解用户请求一个站点,浏览器准备去渲染页面这个过程,也就是导航。

导航始于 Browser 进程

我们在第一篇文章( CPU、GPU、内存以及多进程架构 )中提到过,除了页面显示区域其他部分基本都由 Browser 进程控制。 Browser 进程主要有以下几个线程:

线程名 作用
UI 线程 画出浏览器头部按钮,和一个导航栏输入框
network 线程 处理从网络上获取的内容,通过栈的方式进程存储
storage 线程 处理操作系统的文件权限
... ...

用户能在浏览器的导航栏中输入内容,就是 UI 线程在发挥作用。

图解浏览器主线程内容

一次简单的导航

step1 处理输入

当用户在导航栏输入内容时,UI 线程要做的第一步就是判断出该内容是一个 url 地址还是用户想搜索的内容。因为在 Chrome 中,用户可以直接在导航栏输入想要搜索的内容,因此 UI 线程就需要判断用户的输入是需要发送给搜索引擎还是将页面导航到相应站点。

UI 线程判断用户输入

step2 开始导航

当用户按下 enterUI 线程会初始化一个网络连接获取站点内容,并且在标签左上角显示加载动画。于此同时,网络线程通过适当的协议 (DNS),为请求建立 TLS 链接。

UI 线程和网络线程通信将页面导航到 mysite.com

在导航的过程中,网络线程有可能会收到服务器返回的重定向头部比如:HTTP 301。在这种情况下,网络线程会通知 UI 线程服务器进行了重定向,接着 UI 线程又重新初始化一个网络连接获取重定向站点的内容。

step3 读取响应内容

当网络线程获取到响应内容时,通常情况下它会先读取响应头的内容。一般情况下可以通过响应头中的 Content-Type 字段确定响应正文的内容类型。一旦出现该字段内容遗漏或是错误的情况,MIME 类型嗅探就开始发挥作用。但该策略的实现相当复杂,具体可以查看 Chrome 中的源码,通过阅读源码你可以知道不同浏览器是如何确定 Content-Type 的。

响应的结构,Content-Type 以及真实的数据

如果响应的数据是一个 html 文件,下一步就是将该数据交给渲染进程,但是如果是一个 zip 文件(或其他),说明这是一次下载请求,就会将数据交给下载管理器处理。

当网络线程获取到响应数据时,它会进行安全性判断,如果该站点或是响应内容匹配到已知的恶意站点,网络线程会弹出一个警告页面。此外,网络线程还会进行跨域检查 (Cross Origin Read Blocking (CORB)) ,确保跨站点的数据不会进入渲染进程。

网络线程判断该响应是否来着安全站点

step4 查找渲染进程

一旦所有的判断都结束,那么网络线程就可以确认浏览器可以导航到该站点。网络线程会通知 UI 线程,数据已准备完毕。之后 UI 线程就会通知渲染进程渲染页面。

网络线程通知 UI 线程数据准备完毕,可以进行渲染

当网络线程花费上千毫秒获取响应时,UI 进程同时也会做一些策略上的优化。

由于请求是一个耗时的过程,会照成时间上的浪费,在此期间,UI 线程是知道浏览器需要导航的页面的,该线程就会在导航时提前准备一个渲染进程。这样,当网络线程获取到内容后,UI 进程直接使用已经准备好的渲染进程即可。但是如果服务器有跨站点的重定向,那么准备的进程可能使用不了,这时候就需要使用其他的渲染进程了。

step5 提交导航

通过前几步,站点的数据和渲染进程已经准备完毕, IPC 通道也准备完毕,IPC 将浏览器进程中的响应数据 (HTML DATA) 通过流的形式传递到渲染进程,当数据传输完毕,那么一次导航就结束了,接下去就是渲染进程开始加载文档。

于此同时,地址栏将会更新,安全指示器 (https) 和地址栏的 UI 会反映出站点的信息。历史记录会被更新,前进后退按钮也会跳转到正确的地址。同时浏览器会将存储在本地磁盘中的导航信息更新,以确保在选项卡/浏览器关闭后,再次打开能够获取到相应的导航记录。

IPC 负责将数据从浏览器进程传输到渲染进程

额外的步骤 加载完成

一旦导航被提交,渲染进程会接收到数据并渲染页面。在下一章中,我们会介绍这一过程。

一旦渲染进程将页面渲染“完毕” (渲染进程解析玩数据,显示页面,并执行完所有的 onload 回调后),它会开启一个 IPC ,通知浏览器进程页面加载完毕。浏览器进程接收到信息后就会停止选项卡上的 loading 状态。

注: 这里的“完毕”之所以加引号,因为客户端的 javascript 能够继续加载资源,并在资源加载后呈现出新的页面内容。

渲染进程通过 IPC 告知浏览器进程加载完毕

导航到其他站点

通过以上的步骤,一次简单的导航已经完成。但是如果用户在地址栏输入了不同的地址会发生什么呢?浏览器会通过相同的步骤将页面导航至用户输入的站点,但是在此之前,它需要查看当前显示网站是否注册了 beforeunload 事件。

当用户想更换导航地址或是关闭选项卡时,站点页面能够通过 beforeunload 事件设置 "离开当前站点吗?" 之类的弹窗,用于提示用户。但是选项卡内的所有内容包括 javascript 代码,都是由渲染进程处理的,因此当浏览器进程发起新的导航时,需要与当前页面的渲染进程进行交互。

注意:不要无条件的添加 beforeunload 事件,由于这需要进程间的通信,过多的 beforeunload 事件会照成延迟,因此该事件应该在需要的时候添加。比如,当用户切换导航时,原有页面上用户的输入数据将会丢失。
浏览器进程通过 IPC 通知渲染进程执行 beforeunload 事件注册的回调

如果,导航的切换发生在渲染进程内部 (比如用户点击 a 标签,或是触发了 window.location = "https://newsite.com"),渲染进程首先触发 beforeunload 事件,接着渲染进程会通知浏览器进程导航切换。

当新导航与当前站点为不同的站点时,新的导航会生成新的渲染进程,同时旧的渲染进程会在后台处理一些事件比如 (unload)。你可以通过an overview of page lifecycle states,来了解页面的生命周期,以及可以使用的事件。

浏览器进程通过 IPC 通知新老渲染进程执行不同的任务

Service Worker

最近一个重要的变化就是浏览器实现了 Service WorkerService worker 可以通过代码来实现网络代理;程序开发者也可以通过 Service worker 来缓存从网络上获取的数据。如果网站使用了 Service worker 那么该页面就可以从缓存中拿去数据,而不需要从网络上拉取。

但需要注意的是,Service worker 是用 javascript 代码写的,而 javascript 是在渲染进程中执行的,但是导航却发生在浏览器进程,那么浏览器进程是如何知道该站点注册了 Service worker

Service worker 注册成功后,该 Service worker 的作用域将会被保存下来(具体可查看 Service worker 的生命周期),当导航发生时,网络线程会去确认该站点是否注册了 Service worker,如果注册了,那么 UI 线程创建渲染进程,去执行 Service worker 的相关代码。该 Service worker 就会从缓存中获取数据,而非通过网络获取。当然如果资源未缓存就会从网络上拉取。

浏览器进程中的网络线程查看该站点是否注册了 Service worker
浏览器进程中的 UI 线程创建渲染进程并执行 Service worker

导航预加载

当页面注册了 Service worker ,但是相关的资源却并没有被缓存,那么渲染进程就得通知浏览器进程去获取数据,而进程间的通信会照成一定程度的延时。为了减轻延时的影响,导航预加载就是一种有效的策略。该策略会在 Service worker 执行的同时从网络加载数据,从而减轻延时的影响。它通过设定特定的请求头,允许服务器决定返回的数据。比如浏览器仅需要 HTML 文档的一部分而不是整个文档。

浏览器进程中的 UI 线程在通知渲染进程处理 Service worker 的同时通知网络线程获取数据

总结

在这篇文章中,我们了解到在一次导航中发生了什么以及一个站点中的 javascript 代码是如何影响到浏览器的各个进程的。在知道了浏览器从网络上获取数据的具体步骤的情况下,我们更能轻松的理解浏览器为什么要进程导航预加载。在下一篇文章中,我们将深入渲染进程,了解浏览器是如何通过 HTML/CSS/JavaScript 代码渲染出一个页面的。

相关文章