深入理解现代浏览器 2: Navigation 过程分析
目录
译注:本文翻译自 ChromeDeveloper 博客, 原文发表于2018年底, 部分特性可能与目前 Chrome 有所不同。建议有条件的读者直接阅读原文。
本文是深入理解现代浏览器4章中的第2章。此前我们研究了不同进程和线程是如何处理浏览器的不同部件的, 本章中, 我们来深挖每个进程和线程间是如何通信以显示网站的。
我们来看一个简单浏览器网站的用例:在浏览器中输入URL, 然后浏览器从网络上获取数据并显示页面。本章中, 我们聚焦于用户请求站点并准备呈现页面这一部分--即我们说的导航(Navigation)。
从浏览器进程开始说起
如我们之前所说,标签以外的所有内容都由 browser 进程处理。browser 进程拥有很多线程, 像绘制按钮和输入域的 UI 线程,和网络栈交换信息以从网上接收数据的 network 线程,控制文件访问的 storage 线程等等。当你在地址栏输入URL时, browser 进程的 UI线程会处理你的输入。
简单的导航
Step 1: 处理输入
当用户开始在地址栏中输入时,UI线程首先会问"这是一搜索查询还是URL"。 在Chrome 中, 地址栏也是搜索输入域,所以UI线程需要解析并确认是使用搜索引擎还是请求站点。
Step 2: 开始导航
当用户点击回车,UI 线程启动一个网络调用并获取站点内容。标签的左上角会显示加载动画,并且 network 线程这通过适当的协议来处理请求, 如 DNS查找与TSL连接建立。
此时, network 线程可能收到像HTTP301 这样的服务重定向头。这种情况下, network 线程和UI线程交流, 服务请求重定向, 之后开始另一个URL请求。
Step 3: 读取 Response
一旦 response body(payload)流开始进入,如有必要, network 线程查找该流的前几个字节。 response body 的 Content-Type 头会指示数据的类型, 但因为该它可能丢失或是错误的, MIME 类型嗅探 会在此处完成。正如源码中所说, 这是一项“棘手的业务”。你可以阅读代码注释, 了解不同的浏览器是如何处理 content-type/payload
的。
如果 response 是一个 HTML 文件 , 那么下一步就是将数据传递给给渲染进程 。但它若是一个 zip 或其它什么文件, 就意味着这是个下载请求,那么这些它们会将数据传递给下载管理器。
“安全浏览检查”也发生在此处。如果域名或response 数据看带来和已知的恶意网站相匹配, 那么network线程会提醒并显示一个警告页面。此外,这里还会进行 跨站点读取阻塞(Cross Origin Read Blocking (CORB)
检查, 来确保敏感的跨站点数据不会进行渲染进程。
Step 4: 查找渲染进程
当所有的检查都完成, 且 network 线程确信浏览器应该导航到请求的站点之后 ,network 线程告诉 UI线程数据准备好了,然后UI 线程查找并获取一个渲染进程来渲染网页。
由于网络请求可能耗时数百毫秒获取 response,这里会进行优化以加快此过程。当 Step 2, UI线程发送URL请求到 network 线程时,就已经知道会导航到哪个站点了, UI线程会并行地主动查找, 或启动一个渲染进程。这样如果不出意外, 当network 线程收到数据时, 一个渲染进程已经处理待机位置了。如果导航重跨站点重定向, 这个待机进程可能不会被使用, 这种情况会需要另一个不同的进程。
Step 5: 提交导航
至此, 数据和渲染进程都准备妥当, 提交导航的 IPC 也从 browser 进程发送到 渲染进程。同时还通过流传递了数据,所以渲染进程仍可以继续接收 HTML数据。一旦browser 进程听到渲染进程中确认产生了提交, 导航就完成了,同时开始了文档加载阶段。
此时, 地址栏被更新, 安全标识和站点设置UI 会指示这个新页面的站点信息。标签的历史记录被更新,以便 前进/后退 按钮可以逐步浏览刚刚导航的站点。当关闭标签或窗口时,会话历史会存盘到硬盘上以便还原标签。
附加 Step: 初始化加载完成
一旦提交了导航, 渲染进程开始加载资源并渲染页面。下一章我们将详细了解这一步里发生了什么 。当渲染进程完成("finishes")渲染,它将向 browser 进程发回IPC(在页面中所的有frame 的 onload
事件触发并执行完成以后)。此时UI线程会停止标签上的加载动画。
这里说 "finishes" (而不是 "finished" ), 是因为在这之后,客户端的 Javascript 可能仍在加载附加资源,并渲染新的view。
导航到不同的站点
我们完成了一个简单的导航。但如果用户在地址栏输入了不同的URL, 会发生什么 呢?browser 进程会通过和之前一样的步骤导航到不同的站点, 但在此之前, 它需要检查当前渲染的站点,是否需要关心 beforeunload
事件。
当你试图导航离开或关闭标签时,beforeunload
会创建一个 “要离开本站吗”的提醒。标签内所有的内容, 包括 JavaScript代码, 都由渲染进程处理, 所以当一个新的导航请求进入时,browser 进程会检查当前的渲染进程。
警告 : 不要添加无条件的
beforeunload
处理器,因为这些处理器会在导航开始之前执行,从而导致更多的延迟。事件处理器应只在需要时添加,比如需要提醒用户进入新页面前可能会丢失数据。
如果导航是从渲染进程启动的(比如用户点击了一个链接,或客户端JavaScript运行了 window.location = "https://newsite.com"
),渲染进程会先检查 beforeunload
处理器,然后它将经历与 browser 进程启动导航相同的过程, 唯一的区别是导航请求是从渲染进程到browser 进程。
当新导航的站点与当前渲染进程里的站点不同时,将调用一个单独的渲染进程来处理它, 同时会保留当前的渲染进程以处理像 unload 之类的事件。参见 页面生命周期概览,以及如何使用 页面生命周期API hook .
Service Worker
此导航过程的最新更新是引入了 Service Worker. Service Worker 是一种有你的应用程序代码中编写网络代理的方法, 它允许web开发者更好的控制本地缓存以及何时从网络获取数据。如果设置 ServiceWorker从缓存中加载页面,那么就不必从网络请求数据。
谨记,ServiceWorker 是运行在渲染进程中的JavaScript代码。但导航请求进入时,browser 进程如何知道此站点是否有 ServiceWorker呢?
在注册ServiceWorker时, 将保留ServiceWorker的作用范围的引用(欲知更多关于范围的信息,请参考 Service Worker 生命周期 )。当导航发生时, network 进程会针对ServiceWorker的工作范围检查域名。若为此URL注册了 Service Worker , 那么 UI 线程会查找一个渲染进程以执行 Service Worker 代码。ServiceWorker 可以从缓存中加载数据从而取消从网络请求数据的需求, 亦可从网络请求新资源。
导航预加载
如果Service Worker最终决定从网络请求数据,browser 进程得渲染进程之间的这种往返可能会导致延迟。导航预加载
是一种是在 Service Worker 启动时并行 加载资源, 从而来加速此过程的一种机制。它用一个 header 来标记请求,允许服务器来决定为这些请求发送不同的内容, 比如, 只更新数据而不是整个文档。
小节
本章中, 我们研究了导航中都发生了什么 , 以及你的 web 应用程序代码, 如 response header 和客户端 JavaScript 如何与浏览器交互。了解了浏览器如何从网络获取数据,就更容易理解为何会开始导航预加载 API. 下一章, 我们将深入研究浏览器如何计算(evaluates)我们的 HTML/CSS/JavaScript 来渲染页面。