这是由 4 篇文章组成的系列博客的第二部分,介绍了 Chrome 的内部工作原理。在上一篇文章中,我们研究了不同的进程和线程如何处理浏览器的不同部分。在这篇文章中,我们将更深入地挖掘每个进程和线程为了显示一个网站是如何进行通信的。
让我们来看一个简单的 Web 浏览例子:您在浏览器中输入一个 URL,然后浏览器从互联网获取数据并显示一个页面。在这篇文章中,我们将重点关注用户请求站点和浏览器准备呈现页面的部分--也称为导航。
正如我们在第 1 部分:CPU、GPU、内存和多进程架构中所述,标签页之外的所有内容都由浏览器进程处理。浏览器进程具有多个线程,如绘制浏览器按钮和输入域的 UI 线程、处理网络堆栈以从互联网接收数据的 Network 线程、控制对文件的访问的存储线程等等。当您在地址栏中键入 URL 时,您的输入将由浏览器进程中的 UI 线程处理。
当用户开始在地址栏中输入内容时,UI 线程首先询问的是 “这是搜索查询还是 URL?“。在 Chrome 中,地址栏也是一个搜索输入域,因此 UI 线程需要解析并决定是将您跳转到搜索引擎,还是跳转到您请求的站点。
当用户按 Enter 时,UI 线程启动网络调用以获取站点内容。loading 图标会显示在标签页的一角,Network 线程通过适当的协议,如 DNS 查找并且为请求建立 TLS 连接。
此时,Network 线程可以接收类似于 HTTP 301 的服务器重定向报头。在这种情况下,Network 线程与服务器正在请求重定向的 UI 线程进行通信。然后,将发起另一个 URL 请求。
一旦响应体(有效负载)开始进入,Network 线程将在必要时查看流的前几个字节。响应的 Content-Type 标头应该说明它是什么类型的数据,但由于它可能丢失或错误,因此这里会执行 MIME 类型嗅探。正如源代码中所说,这是一项“棘手的业务”。您可以阅读评论以了解不同的浏览器如何处理 content-type/payload 对。
如果响应是一个 HTML 文件,那么下一步将是将数据传递给渲染器进程,但是如果它是一个压缩文件或其他文件,那么这意味着它是一个下载请求,所以他们需要将数据传递给下载管理器。
这也是进行安全浏览检查的地方。如果域和响应数据似乎与已知的恶意站点匹配,则 Network 线程会发出警报以显示警告页面。
此外,还会执行 Cross Origin Read Blocking (CORB) 检查,以确保敏感的跨站点数据不会进入渲染器进程。
一旦所有检查完成并且 Network 线程确信浏览器应该导航到请求的站点,Network 线程就会告诉 UI 线程数据已经准备好了。然后,UI 线程找到一个渲染器进程来进行网页的呈现。
由于网络请求可能需要数百毫秒才能返回响应,因此应用了加速此过程的优化。当 UI 线程在步骤 2 向 Network 线程发送 URL 请求时,它已经知道它们正在导航到哪个站点。UI 线程尝试与网络请求并行地主动查找或启动渲染器进程。这样,如果一切按预期进行,则当 Network 线程接收数据时,渲染器进程已经处于待命状态。如果导航重定向跨站点,则可能不会使用此备用进程,在这种情况下,可能需要不同的进程。
既然数据和渲染器进程已经准备好,就可以将 IPC 从浏览器进程发送到渲染器进程以提交导航。它还传递数据流,以便渲染器进程可以继续接收 HTML 数据。一旦浏览器进程在渲染器进程中听到提交的确认,导航就完成了,然后就开始了文档加载阶段。
此时,地址栏被更新,安全指示器和站点设置 UI 展示新页面的站点信息。该标签页的会话历史记录将被更新,因此后退/前进按钮将遍历刚刚导航到的站点。为便于在关闭标签页或窗口时恢复标签页/会话,会话历史记录存储在磁盘上。
提交导航后,渲染器进程将继续加载资源并渲染页面。我们将在下一篇文章中详细介绍这一阶段发生的事情。一旦渲染器进程“完成”渲染,它就会将 IPC 发送回浏览器进程(这是在页面中的所有帧上触发了所有 onLoad 事件并完成执行之后)。此时,UI 线程停止标签页左上角的 loading 图标。
我之所以说“完成”,是因为在这之后,客户端的 JavaScript 仍然可以加载额外的资源并渲染新的视图。
简单的导航这样就完成了!但是,如果用户再次将不同的 URL 放到地址栏中会发生什么呢?当然,浏览器进程要经过相同的步骤才能导航到不同的站点。但在它可以这样做之前,它需要检查当前渲染的站点是否关心bepreunLoad事件。
beforeunload
可以创建“离开这个网站吗?”这样一个警告弹窗,当您尝试离开或关闭选项卡时。标签页中的所有内容,包括您的 JavaScript 代码,都是由渲染器进程处理的,所以当新的导航请求进入时,浏览器进程必须检查当前的渲染器进程。
WARNING
不要添加无条件的beforeunload
处理程序。它会造成更多的延迟,因为在启动导航之前需要执行处理程序。只有在需要时才应添加此事件处理程序,例如,如果需要警告用户可能会丢失在页面上输入的数据,则应添加此处理程序。
如果导航是从渲染器进程启动的(比如用户点击了一个链接,或者客户端运行了 window.Location=“https://newsite.com”)),那么渲染器进程会在处理程序之前进行`beforeunload`检查。然后,它经历与浏览器进程启动导航相同的过程。唯一的区别是导航请求从渲染器进程发送到浏览器进程。
当新导航到与当前渲染的站点不同的站点时,将调用单独的渲染进程来处理新导航,同时保留当前渲染进程以处理诸如unload
之类的事件。有关更多信息,请参阅页面生命周期状态概述以及如何使用页面生命周期 API与事件挂钩。
这一导航流程最近的一个变化是引入了Service Worker。Service Worker 是一种在您的应用程序代码中编写网络代理的方法;允许 Web 开发人员更多地控制本地缓存的内容以及何时从网络获取新数据。如果 Service Worker 设置为从缓存加载页面,则不需要从网络请求数据。
需要特别注意的是,Service Worker 是在渲染器进程中运行的 JavaScript 代码。但是,当导航请求进入时,浏览器进程如何知道站点有 Service Worker 呢?
注册 Service Worker 后,将保留 Service Worker 的范围作为参考(您可以在这篇Service Worker 生命周期文章中阅读有关范围的更多信息)。
当导航发生时,网络线程根据注册的 Service Worker 作用域来检查域,如果 Service Worker 注册了该 URL,则 UI 线程找到一个渲染器进程以执行 Service Worker 代码。Service Worker 可以从高速缓存加载数据,从而消除不必要的网络请求,或者它可以从网络请求新的资源。
您可以看到,如果 Service Worker 最终决定从网络请求数据,浏览器进程和渲染器进程之间的这种往返可能会导致延迟。导航预加载是一种通过在服务工作者启动时并行加载资源来加速此过程的机制。它用 header 标记这些请求,允许服务器决定为这些请求发送不同的内容;例如,只发送更新的数据,而不是完整的文档。
在这篇文章中,我们研究了导航过程中发生的事情,以及您的 Web 应用程序代码(如响应头和客户端 JavaScript)如何与浏览器交互。了解浏览器从网络获取数据所经历的步骤,可以更容易地理解为什么要开发导航预加载等 API。在下一篇文章中,我们将深入探讨浏览器如何评估我们的 HTML/CSS/JavaScript 以渲染页面。
你喜欢这个帖子吗?如果你对未来的帖子有任何问题或建议,我很乐意在下面的评论区或 Twitter 上的@kosamari上听到你的意见。
原文链接 inside-browser-part2