深入了解现代网络浏览器

深入了解现代网络浏览器

在深入了解浏览器架构之前,需要掌握的另一个概念是进程和线程。进程可以描述为应用的执行程序。线程是位于进程内并执行其进程程序任何部分的线程。

启动应用时,系统会创建一个进程。程序可能会创建线程来帮助其完成工作,但这不是必需的。操作系统会为进程提供内存“大块”,所有应用状态都保存在该私有内存空间中。当您关闭该应用时,进程也会消失,操作系统也会释放内存。

Pasted image 20231213230742.png

图 5:使用内存空间并存储应用数据的进程示意图

一个进程可以请求操作系统启动另一个进程来运行不同的任务。在这种情况下,系统会将内存的不同部分分配给新进程。如果两个进程需要通信,它们可以使用进程通信 (IPC) 实现。许多应用都以这种方式运作,这样一来,如果工作器进程无响应,则可以重启,而无需停止运行应用不同部分的其他进程。

工作器进程和 IPC

图 6:通过 IPC 进行通信的独立进程示意图

浏览器架构

那么,如何使用进程和线程构建网络浏览器?它既可以是具有许多不同线程的进程,也可以是由多个不同进程(多个线程通过 IPC 进行通信)的进程。

浏览器架构

图 7:不同的浏览器架构中的进程 / 线程图

需要注意的是,这些不同的架构属于实现细节。有关如何构建网络浏览器的标准规范。两个浏览器采用的方法可能完全不同

为了方便阅读本系列博文,我们将使用 Chrome 的最新架构,如图 8 所示。

顶部是浏览器进程协调处理应用不同部分的其他进程。对于渲染程序进程,系统会创建多个进程并将其分配给每个标签页。不久之前,Chrome 会尽可能为每个标签页都提供一个进程;现在,它会尝试为每个网站提供专属进程,包括 iframe(请参阅网站隔离)。

浏览器架构

图 8:Chrome 的多进程架构示意图。“Renderer Process”下会显示多个图层,以表示 Chrome 会针对每个标签页运行多个渲染程序进程。

哪个进程控制着什么?

下表介绍了每个 Chrome 进程及其控制的进程:

流程及控制对象
Browser 控制应用的“Chrome”部分,包括地址栏、书签、后退按钮和前进按钮。
此外,还处理网络浏览器中不可见的特权部分,例如网络请求和文件访问。
渲染程序 控制标签页中显示网站的一切内容。
插件 控制网站使用的所有插件,例如 Flash。
GPU 独立于其他进程处理 GPU 任务。它分为不同的进程,因为 GPU 会处理来自多个应用的请求并在同一 Surface 中绘制它们。

Chrome 进程

图 9:指向浏览器界面不同部分的不同进程

还有更多进程,比如扩展进程和实用程序进程。如果您想查看 Chrome 中正在运行的进程,请点击右上角的选项菜单图标 more_vert,选择“更多工具”,然后选择“任务管理器”。此时将打开一个窗口,其中包含当前正在运行的进程及其占用的 CPU/内存量。

Chrome 中多进程架构的优势

之前我提到过 Chrome 使用多个渲染程序进程。在最简单的情况下,您可以假设每个标签页都有自己的渲染程序进程。假设您打开了 3 个标签页,每个标签页都由独立的渲染程序进程运行。

如果某个标签页无响应,您可以关闭无响应标签页并继续,同时让其他标签页保持活跃状态。如果所有标签页都在一个进程上运行,那么当一个标签页无响应时,所有标签页都无响应。真遗憾。

多个标签页的渲染程序

图 10:该图显示了运行每个标签页的多个进程

将浏览器工作分为多个进程的另一个好处是安全性和沙盒。由于操作系统提供了一种限制进程权限的方法,因此浏览器可以沙盒化某些功能,让特定进程发挥特定功能。例如,Chrome 浏览器会限制处理任意用户输入的进程(例如渲染程序进程)对任意文件的访问权限。

由于进程有自己的专用内存空间,因此进程通常包含通用基础架构的副本(例如,V8,这是 Chrome 的 JavaScript 引擎)。这意味着内存使用量更高,因为它们不能像它们是同一进程中的线程时那样共享。为了节省内存,Chrome 对可以启动的进程数量设置了限制。 该上限因设备的内存和 CPU 性能而异,但当 Chrome 达到上限时,便会开始在一个进程中运行来自同一网站的多个标签页。

节省更多内存 - Chrome 中的服务

该方法也适用于浏览器进程。Chrome 正在进行架构更改,以便将浏览器程序的每个部分作为一项服务来运行,从而允许拆分为不同的进程或聚合为一个进程。

一般来说,当 Chrome 在强大的硬件上运行时,可能会将每项服务拆分为不同的进程,以提高稳定性;但如果是在资源有限的设备上,Chrome 会将各项服务合并为一个进程,从而节省内存占用量。在此变更之前,类似的方法整合进程以减少内存用量,就已经在 Android 等平台上使用。

Chrome 服务化

图 11:Chrome 服务将不同服务移入多个进程和一个浏览器进程的示意图

每帧渲染程序进程 - 网站隔离

网站隔离是 Chrome 中最近推出的一项功能,可为每个跨网站 iframe 运行单独的渲染程序进程。我们一直在讨论每个标签页模型一个渲染程序进程,它允许跨网站 iframe 在单个渲染程序进程中运行,并在不同网站之间共享内存空间。可以在同一渲染程序进程中运行 a.com 和 b.com。同源政策是网络的核心安全模型;它可以确保一个网站在未经同意的情况下无法访问来自其他网站的数据。绕过此政策是安全攻击的主要目标。 进程隔离是隔离网站的最有效方法。在 Meltdown 和 Spectre 中,我们需要使用进程来分隔网站,这一点变得更加明显。从 Chrome 67 开始,系统会在桌面设备上默认启用网站隔离功能,因此标签页中的每个跨网站 iframe 都会获得单独的渲染程序进程。

网站隔离

图 12:网站隔离示意图;多个渲染程序进程都指向网站内的 iframe

启用网站隔离功能是一项需要多年的工程工作。网站隔离不像分配不同的渲染程序进程那样简单,它会从根本上改变 iframe 之间的通信方式。在 iframe 在不同进程上运行的页面上打开开发者工具,意味着开发者工具必须实现后台工作,才能流畅显示。即使运行简单的 Ctrl+F 键查找网页中的某个字词,也要在不同的渲染程序进程中进行搜索。这就是为什么浏览器工程师谈到网站隔离功能的发布是一个主要里程碑的原因!

小结

在这篇博文中,我们对浏览器架构进行了简要的介绍,并介绍了多进程架构的优势。我们还介绍了 Chrome 中的服务化和网站隔离,这与多进程架构密切相关。在下一篇帖子中,我们将开始深入讨论这些进程和线程之间发生的情况,以便显示网站。

您喜欢这个帖子吗?如果您对日后发布的博文有任何疑问或建议,请随时通过 @kosamari 在 Twitter 上告诉我们。

深入了解现代网络浏览器(第 2 部分)

导航中会出现什么情况

本系列博文由 4 部分组成,本文将重点介绍 Chrome 的内部运作方式,本文为第 2 部分。 在上一篇博文中,我们了解了不同进程和线程如何处理浏览器的不同部分。在这篇博文中,我们将深入探讨每个进程和线程如何通信以显示网站。

我们来看一个简单的网络浏览用例:您在浏览器中输入网址,然后浏览器从互联网提取数据并显示网页。在这篇博文中,我们将重点关注用户请求访问网站,然后浏览器准备呈现网页的部分(也称为导航)。

首先是一个浏览器进程

浏览器进程

图 1:顶部的浏览器界面,底部的浏览器进程示意图,界面、网络和存储线程

正如我们在第 1 部分:CPU、GPU、内存和多进程架构中所介绍的,标签页以外的一切都由浏览器进程处理。浏览器进程具有如下线程:用于绘制浏览器的按钮和输入字段的界面线程、负责处理网络堆栈以从互联网接收数据的网络线程、用于控制文件访问的存储线程等。当您在地址栏中输入网址时,输入由浏览器进程的界面线程处理。

简单的导航

第 1 步:处理输入

当用户开始在地址栏中输入内容时,界面线程会问的第一个问题是“这是搜索查询或网址吗?”。在 Chrome 中,地址栏也是一个搜索输入字段,因此界面线程需要解析并决定是将您转到搜索引擎还是您请求的网站。

处理用户输入

图 1:询问输入项是搜索查询还是网址的界面线程

第 2 步:开始导航

当用户点击 Enter 键时,界面线程会发起网络调用来获取网站内容。标签页的一角会显示“正在加载”旋转图标,并且网络线程会通过适当的协议(例如 DNS 查找)并为请求建立 TLS 连接。

导航开始

图 2:与网络线程通信以导航到 mysite.com 的界面线程

此时,网络线程可能会收到类似于 HTTP 301 的服务器重定向标头。在这种情况下,网络线程会与服务器请求重定向的界面线程通信。然后,系统将发起另一个网址请求。

第 3 步:阅读回复

HTTP 响应

图 3:包含 Content-Type 和载荷(即实际数据)的响应标头

当响应正文(载荷)开始传入后,网络线程会根据需要查看流的前几个字节。响应的 Content-Type 标头应该会显示具体的数据类型,但由于可能缺失或错误,因此请在此处完成 MIME 类型嗅探。正如源代码中所述,这是一种“棘手的业务”。 您可以阅读相关评论,了解不同浏览器如何处理内容类型/载荷对。

如果响应是 HTML 文件,下一步就是将数据传递给渲染程序进程;但如果它是 ZIP 文件或其他某个文件,则意味着它是一个下载请求,所以它们需要将数据传递给下载管理器。

MIME 类型嗅探

图 4:网络线程询问响应数据是否为来自安全网站的 HTML

系统还会在此处进行SafeBrowsing检查。 如果该网域和响应数据似乎与某个已知的恶意网站匹配,则网络线程会发出提醒并显示一个警告页面。此外,系统会进行CCOrigin Cead Clocking (C) 检查,以确保敏感的跨网站数据不会进入渲染程序进程。

第 4 步:查找渲染程序进程

在完成所有检查且网络线程确信浏览器应导航到所请求的网站后,网络线程会告知界面线程数据已准备就绪。然后,界面线程会找到渲染程序进程来继续渲染网页。

查找渲染程序进程

图 5:告知界面线程查找渲染程序进程的网络线程

由于网络请求可能需要数百毫秒才能收到响应,因此采用了可加快此过程的优化。当界面线程在第 2 步向网络线程发送网址请求时,已经知道要导航到哪个网站。界面线程会尝试主动查找或启动与网络请求并行的渲染程序进程。这样,如果一切按预期进行,渲染器进程在网络线程接收到数据时就已处于待机状态。如果导航跨网站重定向,则可能不会使用此待机进程(在这种情况下,可能需要其他进程)。

第 5 步:提交导航

现在数据和渲染程序进程已准备就绪,浏览器进程会向渲染器进程发送 IPC 以提交导航。它还会传递数据流,以便渲染程序进程可以继续接收 HTML 数据。浏览器进程听到在渲染程序进程中发生提交的确认信息后,导航即告完成,文档加载阶段随即开始。

此时,地址栏会更新,安全指示器和网站设置界面会反映新页面的网站信息。系统会更新该标签页的会话历史记录,以便使用“往返”按钮 逐步跳转到用户刚刚访问过的网站为便于您在关闭标签页或窗口时恢复标签页/会话,系统会将会话历史记录存储在磁盘上。

提交导航

图 6:浏览器与渲染器进程之间的 IPC,用于请求渲染网页

额外步骤:初始加载完成

提交导航后,渲染器进程会继续加载资源并渲染页面。我们会在下一篇博文中详细介绍这一阶段会发生什么。渲染程序进程“完成”渲染后,会将 IPC 发送回浏览器进程(在网页中的所有帧上触发所有 onload 事件并完成执行后)。此时,界面线程会停止标签页上的加载旋转图标。

我说“finishes”,因为在此之后,客户端 JavaScript 仍然可以加载其他资源并渲染新视图。

网页加载完毕

图 7:从渲染器到浏览器进程的 IPC 用于通知网页已“加载”

导航到其他网站

简单的导航已完成!但是,如果用户再次在地址栏中输入不同的网址,会发生什么情况?浏览器进程会执行相同的步骤来导航到不同的网站。 但在此之前,它需要先确认当前呈现的网站是否关注 beforeunload 事件。

beforeunload 可在您尝试离开此网站或关闭标签页时创建“要离开此网站吗?”提醒。 标签页中的所有内容(包括 JavaScript 代码)均由渲染器进程处理,因此当新的导航请求时,浏览器进程必须检查当前的渲染器进程。

注意:请勿添加无条件 beforeunload 处理程序。这会增加延迟时间,因为需要在导航开始之前执行处理程序。请仅在需要时(例如需要收到警告,提醒用户可能会丢失在网页上输入的数据)时添加此事件处理脚本。

beforeunload 事件处理脚本

图 8:从浏览器进程到渲染器进程的 IPC,指示浏览器要导航到其他网站

如果导航是从渲染程序进程启动的(例如用户点击链接或客户端 JavaScript 已运行 window.location = "https://newsite.com"),渲染程序进程会先检查 beforeunload 处理程序。接着,执行与浏览器进程发起的导航相同的流程。唯一的区别在于,导航请求是从渲染器进程发送到浏览器进程。

当新导航前往与当前呈现的网站不同的网站时,系统会调用单独的渲染进程来处理新的导航,同时保留当前渲染进程来处理 unload 等事件。如需了解详情,请参阅页面生命周期状态概览以及如何使用 Page Lifecycle API 接入事件。

新导航和卸载

图 9:2 个从浏览器进程到新渲染程序进程的 IPC,告知系统渲染网页并告知旧的渲染器进程卸载

如果是 Service Worker

此导航过程最近的一项更改是引入了 Service Worker。Service Worker 是一种在应用代码中编写网络代理的方法,可让 Web 开发者更好地控制本地缓存的内容以及何时从网络获取新数据。如果 Service Worker 设置为从缓存加载页面,则无需从网络请求数据。

需要注意的重要部分是,Service Worker 是在渲染程序进程中运行的 JavaScript 代码。但是,当导航请求传入时,浏览器进程如何知道网站有 Service Worker?

Service Worker 范围查找

图 10:浏览器进程中的网络线程查询 Service Worker 范围

注册 Service Worker 后,Service Worker 的作用域将作为参考进行保留(如需详细了解作用域,请参阅 Service Worker 生命周期文章)。导航发生时,网络线程会根据已注册的 Service Worker 作用域检查网域,如果已针对该网址注册了 Service Worker,界面线程会查找渲染程序进程以执行 Service Worker 代码。Service Worker 可以从缓存中加载数据,这样就无需从网络请求数据,也可以从网络请求新资源。

Service Worker 导航

图 11:浏览器进程中的界面线程会启动渲染器进程以处理 Service Worker;然后,渲染器进程中的工作器线程会从网络请求数据

导航预加载

您可以看到,如果 Service Worker 最终决定从网络请求数据,浏览器进程和渲染器进程之间的这种往返可能会导致延迟。Navigation 预加载是一种通过在 Service Worker 启动过程中并行加载资源来加快此过程的机制。它使用标头来标记这些请求,以便服务器决定为这些请求发送不同的内容;例如,只更新数据而不是完整文档。

Navigation 预加载

图 12:浏览器进程中的界面线程会启动渲染程序进程来处理 Service Worker,同时并行启动网络请求

小结

在这篇博文中,我们介绍了导航期间发生的情况,以及您的 Web 应用代码(例如响应标头和客户端 JavaScript)如何与浏览器互动。了解浏览器从网络获取数据所经历的步骤,可以更轻松地理解导航预加载等 API 的开发原因。在下一篇博文中,我们将深入探讨浏览器如何评估 HTML/CSS/JavaScript 以呈现网页。

您喜欢这个帖子吗?如果您对日后发布的博文有任何疑问或建议,欢迎随时通过下方的评论部分或 Twitter 上的 @kosamari 提出您的想法。

深入了解现代网络浏览器(第 3 部分)

小作幸子

Twitter

渲染程序进程的内部工作原理

本系列博文共 4 部分,本文将探讨浏览器工作方式,本文为第 3 部分。之前,我们介绍了多进程架构导航流。在本博文中,我们将了解渲染程序进程内部的情况

渲染程序进程会影响网页性能的很多方面。由于渲染程序进程内会进行很多操作,因此本博文只是简要概述。如需深入了解,请参阅“网站开发基础”的“性能”部分,其中提供了更多资源。

渲染程序进程处理网页内容

渲染程序进程负责标签页内发生的一切。在渲染程序进程中,主线程会处理您发送给用户的大多数代码。有时,如果您使用 Web Worker 或 Service Worker,则部分 JavaScript 将由工作器线程处理。合成器和光栅线程也会在渲染程序进程内运行,以便高效、流畅地渲染页面。

渲染程序进程的核心任务是将 HTML、CSS 和 JavaScript 转换为用户可与之互动的网页。

渲染程序进程

图 1:内部的主线程、工作器线程、合成器线程和光栅线程的渲染程序进程

解析

DOM 的构建

当渲染器进程收到导航的提交消息并开始接收 HTML 数据时,主线程开始解析文本字符串 (HTML),并将其转换为一个对象态 (DOM)。

DOM 是浏览器的内部网页表示形式,也是 Web 开发者可通过 JavaScript 与之交互的数据结构和 API。

将 HTML 文档解析为 DOM 由 HTML 标准定义。您可能已经注意到,将 HTML 馈送到浏览器绝不会抛出错误。例如,缺少 </p> 结束标记是有效的 HTML。系统会将诸如 Hi! <b>I'm <i>Chrome</b>!</i>(b 标记在 i 标记之前结束)之类的错误标记视为您编写了 Hi! <b>I'm <i>Chrome</i></b><i>!</i>。这是因为 HTML 规范旨在妥善处理这些错误。如果您很好奇这些操作是如何完成的,请参阅 HTML 规范的“An Introduction to Error process and strange case in parser”(解析器中的错误处理和异常情况简介)部分。

正在加载子资源

网站通常使用图片、CSS 和 JavaScript 等外部资源。这些文件需要从网络或缓存加载主线程在解析以构建 DOM 时,可以逐个请求这些元素,但为了加快速度,系统会并发运行“预加载扫描程序”。如果 HTML 文档中包含 <img> 或 <link> 等内容,预加载扫描器会查看由 HTML 解析器生成的令牌,并将请求发送到浏览器进程中的网络线程。

DOM

图 2:解析 HTML 并构建 DOM 树的主线程

JavaScript 可能会阻止

当 HTML 解析器找到 <script> 标记时,它会暂停解析 HTML 文档,并必须加载、解析并执行 JavaScript 代码。为什么呢?因为 JavaScript 可以使用 document.write() 之类的方式改变文档的形状,而 document.write() 会改变整个 DOM 结构(HTML 规范中的解析模型概览有一个很好的示意图)。因此,HTML 解析器必须先等待 JavaScript 运行完毕,然后才能继续解析 HTML 文档。如果您想了解 JavaScript 执行过程中会发生什么,V8 团队会对此发表演讲和撰写博文

提示浏览器应如何加载资源

Web 开发者可以通过多种方式向浏览器发送提示,以便妥善加载资源。如果您的 JavaScript 未使用 document.write(),您可以将 async 或 defer 属性添加到 <script> 标记中。然后,浏览器会异步加载并运行 JavaScript 代码,也不会阻止解析。您也可以使用 JavaScript 模块(如果适用)。<link rel="preload"> 用于通知浏览器当前导航确实需要该资源,而您希望尽快下载该资源。如需了解详情,请参阅资源优先级 - 让浏览器为您提供帮助

样式计算

拥有 DOM 并不足以了解网页是什么样子,因为我们可以通过 CSS 设置页面元素的样式。主线程解析 CSS 并确定为每个 DOM 节点计算出的样式。了解根据 CSS 选择器将何种样式应用于每个元素。您可以在开发者工具的 computed 部分查看此信息。

计算样式

图 3:解析 CSS 以添加计算样式的主线程

即使您未提供任何 CSS,每个 DOM 节点都会有计算出的样式。<h1> 标记的显示尺寸大于 <h2> 标记,且每个元素都定义了外边距。这是因为浏览器有默认样式表。如果您想了解 Chrome 的默认 CSS 是什么样子,可以点击此处查看源代码

布局

现在,渲染程序进程已经知道文档的结构以及每个节点的样式,但这不足以渲染网页。假设您正尝试通过电话向朋友描述一幅画。“一个大的红色圆圈和一个小的蓝色方块”不足以让您的好友知道这幅画到底是什么样子。

人类传真机游戏

图 4:一个人站在一幅画前,用电话连接到另一个人

布局是查找元素几何形状的过程。主线程会遍历 DOM 和计算出的样式,并创建包含 x y 坐标和边界框大小等信息的布局树。布局树的结构可能与 DOM 树类似,但它仅包含与页面上可见内容相关的信息。如果应用 display: none,则该元素不属于布局树的一部分(但是,具有 visibility: hidden 的元素位于布局树中)。同样,如果应用了内容类似于 p::before{content:"Hi!"} 的伪类,则它会包含在布局树中(即使它不在 DOM 中)。

layout

图 5:主线程经过计算的样式并生成布局树的 DOM 树

图 6:因换行而移动的段落的框布局

确定页面布局是一项具有挑战性的任务。即使是最简单的页面布局(例如从顶部到底部的块状流),也必须考虑字体的大小和换行位置,因为这些设置会影响段落的大小和形状,进而影响下一个段落的放置位置。

CSS 可以使元素悬浮在一侧、遮盖溢出项以及更改书写方向。可以想象一下,此布局阶段具有一项强大的任务。在 Chrome 中,整个工程师团队都会处理布局问题。如果您想详细了解他们的工作,可以观看 BlinkOn Conference 的几个讲座,而且非常有趣。

颜料

绘画游戏

图 7:一个人拿着画笔坐在画布前,不知道该先画圆形还是方形

拥有 DOM、样式和布局仍然不足以渲染网页。假设您正试图再现一幅画您知道元素的大小、形状和位置,但仍需要判断绘制元素的顺序。

例如,可能会为某些元素设置 z-index,在这种情况下,按 HTML 中编写的元素顺序绘制会导致呈现错误。

Z-index 失败

图 8:页面元素按 HTML 标记的顺序显示,导致渲染的图片不正确,因为未考虑 Z-index

在此绘制步骤中,主线程会遍历布局树以创建绘制记录。绘制记录是绘制过程的备注,例如“先背景,然后是文本,最后是矩形”。如果您使用 JavaScript 在 <canvas> 元素上绘制,那么您可能很熟悉此过程。

赛车记录

图 9:遍历布局树并生成绘制记录的主线程

更新渲染流水线的成本高昂

图 10:DOM+样式、布局和绘制树(按生成顺序排列)

在渲染流水线中,最重要的一点是,在每一步中,前一个运算的结果都会用于创建新数据。例如,如果布局树发生变化,则需要为文档的受影响部分重新生成绘制顺序。

如果您要为元素添加动画效果,浏览器必须在每一帧之间执行这些操作。大多数显示屏每秒会刷新屏幕 60 次 (60 fps);当每帧在屏幕上移动内容时,动画对人眼来说会很流畅。但是,如果动画在动画中间缺失帧,页面就会出现“卡顿”。

因缺失帧而导致卡顿

图 11:时间轴上的动画帧

即使您的渲染操作能够跟上屏幕刷新的速度,这些计算也是在主线程上运行,这意味着当应用运行 JavaScript 时,这些计算可能会被阻止。

JavaScript 导致 jage 卡顿

图 12:时间轴上的动画帧,但有一帧被 JavaScript 屏蔽

您可以将 JavaScript 操作分成几个小代码块,并使用 requestAnimationFrame() 安排在每一帧中运行。如需详细了解此主题,请参阅优化 JavaScript 执行。您还可以在 Web 工作器中运行 JavaScript,以免阻塞主线程。

请求动画帧

图 13:在包含动画帧的时间轴上运行的较小 JavaScript 数据块

合成

您将如何绘制页面?

图 14:简单光栅过程的动画

现在,浏览器已经知道文档的结构、每个元素的样式、页面的几何图形和绘制顺序,接下来该如何绘制页面呢?将这些信息转换为屏幕上的像素就称为光栅化。

或许,一个简单的处理方法就是对视口内的部分进行光栅处理。如果用户滚动页面,则移动光栅帧,并通过更多光栅化来填充缺失部分。这就是 Chrome 首次发布光栅化时的处理方式。不过,现代浏览器运行着一个更复杂的过程,即合成。

什么是合成

图 15:合成过程的动画

合成是一种技术,可将页面的各个部分分离成图层,单独将其光栅化,然后在单独的线程(称为合成器线程)中合成为页面。如果发生滚动,由于图层已经光栅化,您只需合成一个新帧即可。通过移动层和合成新帧,可以采用相同的方式实现动画。

在开发者工具中,您可以使用“图层”面板查看如何将网站划分为多个图层。

分成多个图层

为了找出哪些元素需要位于哪些层,主线程会遍历布局树来创建层树(此部分在开发者工具的“性能”面板中称为“更新层树”)。如果网页中本应成为单独图层的某些部分(例如滑入式侧边菜单)没有显示该部分,那么您可以在 CSS 中使用 will-change 属性来提示浏览器。

层树

图 16:遍历布局树生成层树的主线程

您可能想要为每个元素都赋予层,但与过多的层进行合成可能会导致操作速度比每帧光栅化地对网页的一小部分进行光栅化,因此衡量应用的渲染性能至关重要。如需详细了解该主题,请参阅坚持仅合成器的属性和管理层数

主线程以外的光栅图像和合成图像

图层树创建完毕并确定绘制顺序后,主线程会将该信息提交到合成器线程。然后,合成器线程会光栅化每个图层。图层可能像页面的整个长度一样大,因此合成器线程会将其划分为图块,并将每个图块发送到光栅线程。光栅线程会光栅化每个图块并将其存储在 GPU 内存中。

光栅

图 17:创建图块位图并将其发送到 GPU 的光栅线程

合成器线程可以优先处理不同的光栅线程,以便首先对视口(或附近)中的内容进行光栅化。一个图层还具有多个适用于不同分辨率的平铺,以处理放大操作等任务。

将图块进行光栅化后,合成器线程会收集称为“绘制四边形”的图块信息,以创建合成器帧。

绘制四边形 包含功能块在内存中的位置,以及考虑到页面合成的情况下要绘制功能块在页面中的位置等信息。
合成器框架 一组绘制四边形,表示页面一个框架。

然后通过 IPC 将合成器帧提交到浏览器进程。此时,对于浏览器界面更改,可以从界面线程添加另一个合成器帧,对于扩展程序,可以从其他渲染程序进程添加。这些合成器帧会发送到 GPU 以在屏幕上显示。如果滚动事件传入,合成器线程会创建另一个要发送到 GPU 的合成器帧。

合成

图 18:创建合成帧的合成器线程。先将帧发送到浏览器进程,然后再发送到 GPU

合成的优势在于,它在完成时不涉及主线程。合成器线程不需要等待样式计算或 JavaScript 执行。因此,仅合成动画被认为是实现流畅性能的最佳方式。如果需要再次计算布局或绘制,则必须涉及主线程。

小结

在这篇博文中,我们了解了渲染流水线从解析到合成的整个过程。希望您现在能够阅读更多有关网站性能优化的内容。

在本系列的下一篇和最后一篇博文中,我们将更详细地介绍合成器线程,并了解当 mouse move 和 click 等用户输入传入时会发生什么。

您喜欢这个帖子吗?如果您对日后发布的博文有任何疑问或建议,欢迎通过下方的评论部分或 Twitter 上的 @kosamari 告诉我们。

深入了解现代网络浏览器(第 4 部分)

小作幸子

Twitter

输入将传送到合成器

本系列博文共 4 部分,深入探讨 Chrome 内部,本文将为您介绍 Chrome 如何处理用于显示网站的代码。在上一篇博文中,我们介绍了渲染过程并了解合成器。在本博文中,我们将介绍合成器如何在用户输入内容时实现流畅的互动。

来自浏览器的输入事件

当您听到“输入事件”时,您可能只会想到在文本框内输入内容或点击鼠标,但从浏览器的角度来看,输入是指用户执行的任何手势。鼠标滚轮滚动是一种输入事件,触摸或鼠标悬停也是输入事件。

当发生用户手势(如轻触屏幕)时,浏览器进程是最先收到该手势的进程。不过,浏览器进程只会知道该手势的发生位置,因为标签页中的内容是由渲染程序进程处理的。因此,浏览器进程会将事件类型(例如 touchstart)及其坐标发送到渲染程序进程。渲染程序进程通过查找事件目标并运行附加的事件监听器来适当地处理事件。

输入事件

图 1:通过浏览器进程路由到渲染程序进程的输入事件

合成器接收输入事件

图 2:悬停在页面图层上的视口

在上一篇博文中,我们了解了合成器如何通过合成光栅化图层来流畅地处理滚动。如果页面未附加任何输入事件监听器,合成器线程可以完全独立于主线程创建新的复合帧。但是,如果页面中附加了一些事件监听器,该怎么办?合成器线程如何确定是否需要处理该事件?

了解不可快速滚动的区域

由于运行 JavaScript 是主线程的作业,因此在合成页面时,合成器线程会将附加了事件处理程序的页面区域标记为“非快速滚动区域”。有了这些信息,合成器线程可以确保在输入事件发生时将该输入事件发送到主线程。如果输入事件来自此区域之外,则合成器线程会继续合成新帧,而不会等待主线程。

有限的非快速滚动区域

图 3:非快速可滚动区域的上述输入示意图

编写事件处理脚本时请注意

Web 开发中常见的事件处理模式是事件委托。由于事件会以气泡形式显示,因此您可以在最顶层的元素附加一个事件处理脚本,并根据事件目标委派任务。您可能已经看到或编写过如下代码。

document.body.addEventListener('touchstart', event => {&nbsp; &nbsp; if (event.target === area) {&nbsp; &nbsp; &nbsp; &nbsp; event.preventDefault();&nbsp; &nbsp; }});

由于您只需为所有元素编写一个事件处理脚本,因此此事件委托模式的工效学设计非常具有吸引力。不过,如果您从浏览器的角度查看此代码,就会发现整个页面现在会被标记为非快速滚动区域。这意味着,即使应用并不关注来自页面某些部分的输入,合成器线程也必须与主线程通信,并在每次有输入事件传入时等待。因此,合成器的流畅滚动功能会受到影响。

整页不可快速滚动区域

图 4:覆盖整个页面的非快速可滚动区域的所描述输入示意图

为避免发生这种情况,您可以在事件监听器中传递 passive: true 选项。这会提示浏览器您仍想监听主线程中的事件,但合成器也可以继续合成新帧。

document.body.addEventListener('touchstart', event => {&nbsp; &nbsp; if (event.target === area) {&nbsp; &nbsp; &nbsp; &nbsp; event.preventDefault()&nbsp; &nbsp; }&nbsp;}, {passive: true});

检查活动是否可以取消

页面滚动

图 5:部分页面固定为水平滚动的网页

假设您在网页上有一个框,您希望将滚动方向限制为仅水平滚动。

在指针事件中使用 passive: true 选项意味着页面可以流畅滚动,但垂直滚动可能在您希望 preventDefault 时开始,以限制滚动方向。您可以使用 event.cancelable 方法对此进行检查。

document.body.addEventListener('pointermove', event => {&nbsp; &nbsp; if (event.cancelable) {&nbsp; &nbsp; &nbsp; &nbsp; event.preventDefault(); // block the native scroll&nbsp; &nbsp; &nbsp; &nbsp; /*&nbsp; &nbsp; &nbsp; &nbsp; * &nbsp;do what you want the application to do here&nbsp; &nbsp; &nbsp; &nbsp; */&nbsp; &nbsp; }}, {passive: true});

或者,您也可以使用 CSS 规则(如 touch-action)来完全删除事件处理脚本。

#area {&nbsp; touch-action: pan-x;}

查找事件目标

点击测试

图 6:查看绘制记录的主线程,询问在 x.y 点上绘制了什么

当合成器线程向主线程发送输入事件时,首先要运行的是查找事件目标的命中测试。点击测试会使用在渲染过程中生成的绘制记录数据,找出事件发生点坐标下方的内容。

尽量减少分派给主线程的事件

在上一篇博文中,我们讨论了典型的显示屏每秒刷新 60 次屏幕,以及如何跟上流畅动画的节奏。对于输入,典型触摸屏设备每秒传送触摸事件 60-120 次,典型鼠标每秒传送事件 100 次。输入事件的保真度高于屏幕可以刷新的保真度。

如果像 touchmove 这样的连续事件每秒发送到主线程 120 次,则可能会触发过多的命中测试和 JavaScript 执行(与屏幕刷新速度相比)。

未经过滤的事件

图 7:在帧时间轴上泛洪导致页面卡顿的事件

为了最大限度地减少对主线程的过多调用,Chrome 会合并连续事件(例如 wheelmousewheelmousemovepointermovetouchmove),并将调度延迟到下一个 requestAnimationFrame 之前。

合并的事件

图 8:与之前相同的时间轴,但事件合并并延迟

系统会立即分派 keydownkeyupmouseupmousedowntouchstart 和 touchend 等离散事件。

使用 getCoalescedEvents 获取帧内事件

对于大多数 Web 应用,合并事件应足以提供良好的用户体验。不过,如果您要构建应用以及根据 touchmove 坐标设置路径等,为了绘制平滑线条,可能会丢失两者之间的坐标。在这种情况下,您可以在指针事件中使用 getCoalescedEvents 方法来获取有关这些合并事件的信息。

getCoalescedEvents

图 9:左侧是平滑触摸手势路径,右侧是合并的有限路径

window.addEventListener('pointermove', event => {&nbsp; &nbsp; const events = event.getCoalescedEvents();&nbsp; &nbsp; for (let event of events) {&nbsp; &nbsp; &nbsp; &nbsp; const x = event.pageX;&nbsp; &nbsp; &nbsp; &nbsp; const y = event.pageY;&nbsp; &nbsp; &nbsp; &nbsp; // draw a line using x and y coordinates.&nbsp; &nbsp; }});

后续步骤

在本系列视频中,我们介绍了网络浏览器的内部工作原理。如果您未曾想过开发者工具为何建议在事件处理脚本中添加 {passive: true},或者为何可能会在脚本标记中编写 async 属性,希望本系列文章能帮助您解释为什么浏览器需要这些信息来提供更快、更顺畅的网络体验。

使用 Lighthouse

如果您希望让代码更适合浏览器使用,但却不知道从何处着手,可以使用 Lighthouse 这款工具对任何网站进行审核,并为您提供有关正确做法和需要改进的报告。浏览审核列表还可以让您了解浏览器关注的方面。

了解如何衡量效果

性能调整可能会因网站而异,因此请务必衡量网站的性能,并确定最适合您网站的方案。Chrome 开发者工具团队提供了一些有关如何衡量网站性能的教程。

向您的网站添加功能政策

如果您想执行额外的步骤,功能政策是一项新的 Web 平台功能,可在构建项目时为您提供保障。开启功能政策可保证您的应用的特定行为,并防止您犯错。例如,如果您希望确保应用永远不会阻止解析,则可以使用同步脚本政策运行应用。启用 sync-script: 'none' 后,系统将阻止执行阻止解析器的 JavaScript。这可以防止您的任何代码阻止解析器,而且浏览器无需费心暂停解析器。

小结

谢谢

开始构建网站时,我几乎只关心如何编写代码以及哪些方法可以提高我的工作效率。这些都很重要,但我们还应考虑浏览器如何接受我们编写的代码。现代浏览器一直在并将继续致力于为用户提供更好的网络体验。通过整理我们的代码来保持对浏览器友好的态度反过来又可以提升您的用户体验。希望您和我一起追求善待浏览器!

衷心感谢审核本系列初稿的所有人,包括(但不限于):Alex RussellPaul IrishMeggin KearneyEric BidelmanMathias BynensAddy OsmaniKinuko Charandiskovli.osuda

你喜欢这个系列吗?如果您对日后发布的博文有任何疑问或建议,欢迎通过下方的评论部分或 Twitter 上的 @kosamari 提出您的想法。