Google Chrome 首次发布是在 2008 年下半年,作为 Windows 平台的测试版。Google 编写的 Chrome 代码也以宽松的 BSD 许可证提供——也被称为 Chromium 项目。对许多观察者来说,这一事件令人惊讶:浏览器大战的回归?Google 真的能做得更好吗?
"它太棒了,以至于它实际上迫使我改变了主意…" - Eric Schmidt,关于他对开发 Google Chrome 的 最初的抵制。
事实证明,他们可以。如今 Chrome 是网络上使用最广泛的浏览器之一(根据 StatCounter,市场份额超过 35%),现在可以在 Windows、Linux、OS X、Chrome OS 以及 Android 和 iOS 平台上使用。显然,Chrome 的功能和功能引起了用户的共鸣,Chrome 的许多创新也已融入其他流行的浏览器。
原始的 38 页漫画书 对 Google Chrome 的理念和创新的解释,提供了对这个流行浏览器背后的思维和设计过程的很好概述。然而,这仅仅是个开始。推动浏览器最初开发的核心原则,仍然是 Chrome 不断改进的指导原则。
正如团队所观察到的,我们今天使用的许多网站不仅仅是网页,它们是应用程序。反过来,越来越雄心勃勃的应用程序需要速度、安全性和稳定性。每一个都值得拥有自己专门的章节,但由于我们的主题是性能,我们的重点将主要放在速度上。
现代浏览器是一个平台,就像您的操作系统一样,Google Chrome 就是这样设计的。在 Google Chrome 之前,所有主要的浏览器都是作为单片、单进程应用程序构建的。所有打开的页面共享相同的地址空间,并争夺相同的资源。任何页面或浏览器的错误都有可能损害整个体验。
相比之下,Chrome 在多进程模型上运行,它提供了进程和内存隔离,以及一个严格的 安全沙箱,用于每个标签页。在一个日益多核的世界中,隔离进程以及保护每个打开的标签页不受其他行为不端的页面影响的能力,证明了 Chrome 在性能方面比竞争对手具有显著优势。事实上,值得注意的是,大多数其他浏览器都效仿了这种做法,或者正在迁移到类似的架构。
在分配了进程后,Web 程序的执行主要涉及三个任务:获取资源、页面布局和渲染以及 JavaScript 执行。渲染和脚本步骤遵循单线程、交错执行的模型——不可能对生成的文档对象模型 (DOM) 进行并发修改。这部分原因是 JavaScript 本身是一种单线程语言。因此,优化渲染和脚本执行运行时如何协同工作至关重要,这对构建应用程序的 Web 开发人员和开发浏览器的开发人员来说都是如此。
对于渲染,Chrome 使用 Blink,它是一个快速、开源且符合标准的布局引擎。对于 JavaScript,Chrome 附带自己的高度优化的 V8 JavaScript 运行时,它也作为独立的开源项目发布,并已融入许多其他流行的项目——例如,Node.js 的运行时。但是,如果浏览器被网络阻塞,等待资源到达,那么优化 V8 JavaScript 执行或 Blink 解析和渲染管道将无济于事。
浏览器优化每个网络资源的顺序、优先级和延迟的能力,是提高整体用户体验的最关键因素之一。您可能没有意识到,Chrome 的网络堆栈实际上每天都在变得更智能,试图隐藏或降低每个资源的延迟成本:它学习可能的 DNS 查找,它记住网络拓扑,它预连接到可能的目的地目标,等等。从外部看,它表现为一个简单的资源获取机制,但从内部看,它是一个精心设计的案例研究,说明如何优化 Web 性能并为用户提供最佳体验。
让我们深入研究。
在我们开始讨论如何优化与网络的交互的战术细节之前,了解我们面临的问题的趋势和格局会有所帮助。换句话说,现代网页或应用程序是什么样的?
The HTTP 存档 项目跟踪 Web 的构建方式,它可以帮助我们回答这个问题。它没有抓取 Web 内容,而是定期抓取最受欢迎的网站,以记录和汇总每个目的地的使用资源数量、内容类型、标头和其他元数据的分析。截至 2013 年 1 月,统计数据可能会让您感到惊讶。在网络上排名前 300,000 个目的地中,一个平均页面是
请仔细思考。平均超过 1 MB 的大小,由图像、JavaScript 和 CSS 等 88 个资源组成,并从 15 个不同的自有和第三方主机提供。此外,这些数字中的每一个都在过去几年中稳步增长,并且没有停止的迹象。我们正在构建越来越大、更雄心勃勃的 Web 应用程序。
将基本数学应用于 HTTP 存档数字表明,一个平均资源大约为 15 KB 大小(1280 KB / 88 个资源),这意味着浏览器中的大多数网络传输都是短暂且突发的。这带来了自己的复杂性,因为底层传输 (TCP) 针对大型流式下载进行了优化。让我们剥开洋葱,检查这些网络请求之一。
The W3C 导航时序规范 提供了一个浏览器 API,可以了解每个浏览器请求生命周期的时序和性能数据。让我们检查一下这些组件,因为它们是提供最佳用户体验的关键部分
图 1.1 - 导航时序
给定网络上资源的 URL,浏览器首先检查其本地和应用程序缓存。如果您之前获取过资源,并且提供了 适当的缓存标头(Expires
、Cache-Control
等),那么您可以使用本地副本来满足请求——最快的请求是不需要进行的请求。或者,如果您需要重新验证资源、资源已过期或您只是从未见过它,那么必须发出一个昂贵的网络请求。
给定主机名和资源路径,Chrome 首先检查现有可重用的打开连接——套接字通过 {scheme, host, port}
进行池化。或者,如果您配置了代理或指定了 代理自动配置 (PAC) 脚本,那么 Chrome 会检查通过相应代理的连接。PAC 脚本允许根据 URL 或其他指定的规则使用不同的代理,每个代理都有自己的套接字池。最后,如果上述条件都不匹配,那么请求必须从解析主机名到其 IP 地址开始——也称为 DNS 查找。
如果我们很幸运,主机名已经在缓存中,在这种情况下,响应通常只需一个快速系统调用即可完成。如果不是,那么必须先发出一个 DNS 查询,然后才能进行任何其他操作。执行 DNS 查找所需的时间将根据您的互联网提供商、网站的流行程度以及主机名在中间缓存中存在的可能性,以及该域的权威服务器的响应时间而有所不同。换句话说,有很多变量在起作用,但 DNS 查找花费几百毫秒并不罕见。哎哟。
图 1.2 - 三次握手
有了解析后的 IP 地址,Chrome 现在可以打开一个新的 TCP 连接到目的地,这意味着我们必须执行“三次握手”:SYN > SYN-ACK > ACK
。这种交换在每个新 TCP 连接中增加了一个完整的往返延迟——没有捷径。根据客户端和服务器之间的距离以及所选的路由路径,这可能会产生从几十毫秒到数百毫秒甚至数千毫秒的延迟。所有这些工作和延迟都是在任何应用程序数据到达网络之前。
一旦 TCP 握手完成,并且如果我们正在连接到一个安全的目的地 (HTTPS),那么必须进行 SSL 握手。这可能会在客户端和服务器之间增加多达两个额外的往返延迟。如果 SSL 会话已缓存,那么我们只需一个额外的往返就可以“逃脱”。
最后,Chrome 能够发出 HTTP 请求(图 1.1 中的 requestStart
)。收到请求后,服务器可以处理请求,然后将响应数据流式传输回客户端。这至少需要一个网络往返,再加上服务器上的处理时间。之后,我们就完成了——除非实际响应是 HTTP 重定向,在这种情况下,我们可能需要重复整个循环一次。在您的页面上有几个无用的重定向吗?您可能需要重新考虑这个决定。
您一直在计算所有延迟吗?为了说明问题,让我们假设一个典型宽带连接的最坏情况:本地缓存未命中,然后是一个相对快速的 DNS 查找(50 毫秒)、TCP 握手、SSL 协商,以及一个相对快速的(100 毫秒)服务器响应时间,往返时间 (RTT) 为 80 毫秒(美国大陆的平均往返时间)。
这对于单个请求来说是 470 毫秒,这意味着网络延迟开销占实际服务器处理时间的一半以上——我们在这方面需要做一些工作。事实上,即使是 470 毫秒也可能是一个乐观的估计
DNS、握手和往返时间的网络开销在我们之前的案例中占主导地位——服务器响应时间仅占总延迟的 20%。但是,从整体来看,这些延迟是否重要?如果你正在阅读本文,那么你可能已经知道答案:是的,非常重要。
过去的 用户体验研究描绘了一幅关于我们作为用户对任何应用程序的响应速度的期望的连贯图景,无论是离线还是在线
延迟 | 用户反应 |
---|---|
0 - 100 毫秒 | 即时 |
100 - 300 毫秒 | 轻微的感知延迟 |
300 - 1000 毫秒 | 机器正在工作 |
1 秒+ | 心理上下文切换 |
10 秒+ | 我稍后再回来... |
表 1.1 还解释了 Web 性能社区中非正式的经验法则:在 250 毫秒内渲染你的页面,或者至少提供视觉反馈,以保持用户的参与。这不仅仅是为了速度而速度。谷歌、亚马逊、微软以及其他数千个网站的研究表明,额外的延迟会直接影响你网站的最终结果:更快的网站会产生更多的页面浏览量,用户参与度更高,转化率也更高。
因此,我们的最佳延迟预算为 250 毫秒,但正如我们在上面的示例中看到的那样,DNS 查询、TCP 和 SSL 握手以及请求的传播时间总共加起来为 370 毫秒。我们超支了 50%,而且我们还没有考虑服务器处理时间!
对于大多数用户甚至 Web 开发人员而言,DNS、TCP 和 SSL 延迟是完全透明的,并且在很少有人会下降或思考的网络层进行协商。但是,这些步骤中的每一个对于整体用户体验都至关重要,因为每个额外的网络请求都可能增加数十甚至数百毫秒的延迟。这就是 Chrome 的网络堆栈远不止一个简单的套接字处理程序的原因。
既然我们已经确定了问题,让我们深入了解实现细节。
Chrome 的多进程架构对于每个网络请求在浏览器中的处理方式具有重要意义。在幕后,Chrome 实际上支持 四种不同的执行模型,这些模型决定了如何执行进程分配。
默认情况下,桌面 Chrome 浏览器使用每个站点一个进程模型,该模型将不同的站点彼此隔离,但将同一站点的所有实例分组到同一个进程中。但是,为了简单起见,让我们假设最简单的情况之一:每个打开的标签页都有一个不同的进程。从网络性能的角度来看,这里差异并不大,但每个标签页一个进程模型更容易理解。
图 1.3 - 多进程架构
该架构为每个标签页指定了一个渲染进程。每个渲染进程都包含 Blink 布局引擎和 V8 JavaScript 引擎的实例,以及连接这些(以及其他几个)组件的胶水代码2。
这些渲染进程中的每一个都在一个沙盒环境中执行,该环境对用户的计算机(包括网络)的访问权限有限。为了访问这些资源,每个渲染进程都与主浏览器(或内核)进程通信,该进程能够对每个渲染器实施安全和访问策略。
Chrome 中渲染器和内核进程之间的所有通信都是通过进程间通信 (IPC) 进行的。在 Linux 和 OS X 上,使用 socketpair()
,它为异步通信提供命名管道传输。来自渲染器的每个消息都会被序列化并传递给一个专用的 I/O 线程,该线程将其分派到主浏览器进程。在接收端,内核进程提供一个过滤器接口,允许 Chrome 拦截资源 IPC 请求(参见 ResourceMessageFilter),这些请求应由网络堆栈处理。
图 1.4 - 进程间通信
这种架构的优点之一是,所有资源请求都在 I/O 线程上完全处理,并且不会受到 UI 生成的活动或网络事件的干扰。资源过滤器在浏览器进程的 I/O 线程中运行,拦截资源请求消息,并将它们转发到浏览器进程中的 ResourceDispatcherHost3 单例。
单例接口允许浏览器控制每个渲染器对网络的访问,但也支持高效且一致的资源共享。一些示例包括
{方案,主机,端口}
(6)组的打开套接字数量施加限制。请注意,这允许最多六个 HTTP 和六个 HTTPS 连接到同一个 {主机,端口}
。就渲染进程而言,它只是通过 IPC 向浏览器进程发送一个资源请求消息,该消息带有唯一的请求 ID,浏览器内核进程负责处理其余工作。
Chrome 网络堆栈实施中的主要问题之一是跨许多不同平台的移植性:Linux、Windows、OS X、Chrome OS、Android 和 iOS。为了应对这一挑战,网络堆栈被实施为一个主要单线程(存在单独的缓存和代理线程)跨平台库,该库允许 Chrome 重用相同的基础设施并提供相同的性能优化,以及跨所有平台进行优化的更大机会。
当然,所有网络代码都是开源的,可以在 src/net
子目录 中找到。我们不会详细检查每个组件,但代码本身的布局告诉你很多关于其功能和结构的信息。表 1.2 中列出了一些示例。
组件 | 描述 |
---|---|
net/android |
与 Android 运行时的绑定 |
net/base |
常见的网络工具,例如主机解析、cookie、网络更改检测和 SSL 证书管理 |
net/cookies |
HTTP cookie 的存储、管理和检索的实施 |
net/disk_cache |
Web 资源的磁盘和内存缓存实施 |
net/dns |
异步 DNS 解析器的实施 |
net/http |
HTTP 协议实施 |
net/proxy |
代理 (SOCKS 和 HTTP) 配置、解析、脚本获取等。 |
net/socket |
TCP 套接字、SSL 流和套接字池的跨平台实施 |
net/spdy |
SPDY 协议实施 |
net/url_request |
URLRequest、URLRequestContext 和 URLRequestJob 实施 |
net/websockets |
WebSockets 协议实施 |
每个组件的代码对于好奇的人来说都是很好的阅读材料——它们有很好的文档,并且你将在每个组件中找到大量的单元测试。
移动浏览器使用量正在呈指数级增长,即使按照最保守的预测,它也将在不久的将来超过桌面浏览。不用说,提供优化的移动体验一直是 Chrome 团队的首要任务。2012 年初,宣布推出 适用于 Android 的 Chrome,几个月后,适用于 iOS 的 Chrome 紧随其后。
首先要注意的是,Chrome 的移动版本并非只是桌面浏览器的直接改编——这样做并不能提供最佳的用户体验。就其本质而言,移动环境的资源约束要大得多,并且具有许多根本不同的操作参数
此外,不存在“典型的移动设备”。相反,存在各种硬件功能不同的设备,为了提供最佳性能,Chrome 必须适应每个设备的操作约束。值得庆幸的是,各种执行模型允许 Chrome 做到这一点。
在 Android 设备上,Chrome 利用与桌面版本相同的多进程架构——有一个浏览器进程,以及一个或多个渲染进程。唯一不同的是,由于移动设备的内存限制,Chrome 可能无法为每个打开的标签页运行一个专用的渲染器。相反,Chrome 会根据可用内存和其他设备约束来确定最佳的渲染进程数量,并在多个标签页之间共享渲染进程。
如果资源有限或 Chrome 无法运行多个进程,它也可以切换到使用单进程多线程处理模型。事实上,在 iOS 设备上,由于底层平台的沙盒限制,它正是这样做的——它运行一个单一的多线程进程。
网络性能如何?首先,Chrome 在 Android 和 iOS 上使用与其他所有版本相同的网络堆栈。这使所有平台都能实现相同的网络优化,从而使 Chrome 获得显著的性能优势。然而,不同的是,通常会根据设备的功能和使用的网络调整一些变量,例如推测性优化技术的优先级、套接字超时和管理逻辑、缓存大小等等。
例如,为了节省电池电量,移动版 Chrome 可能会选择使用空闲套接字的延迟关闭——只有在打开新的套接字时才会关闭套接字,以最大限度地减少无线电使用。同样地,由于预渲染(我们将在下面讨论)可能需要大量的网络和处理资源,因此通常只在用户使用 Wi-Fi 时启用。
优化移动浏览体验是 Chrome 开发团队的最高优先事项之一,我们预计在未来几个月和几年内将看到很多新的改进。事实上,这是一个值得单独章节讨论的主题——也许会在下一期 POSA 系列中出现。
Chrome 在你使用它时会变得更快。这是借助一个名为 Predictor
的单例对象实现的,它在浏览器内核进程中实例化,其唯一职责是观察网络模式,学习和预测未来用户可能的操作。Predictor
处理的一些示例信号包括
Chrome 在你使用它的过程中会学习网络的拓扑结构以及你的浏览模式。如果它能很好地完成工作,它可以消除每次导航的数百毫秒延迟,让用户更接近“即时页面加载”的圣杯。为了实现这一目标,Chrome 利用了表 1.3 中列出的四种核心优化技术。
技术 | 描述 |
---|---|
DNS 预解析 | 提前解析主机名,避免 DNS 延迟 |
TCP 预连接 | 提前连接到目标服务器,避免 TCP 握手延迟 |
资源预取 | 提前获取页面上的关键资源,以加速页面的渲染 |
页面预渲染 | 提前获取整个页面及其所有资源,以便在用户触发时实现即时导航 |
每个调用一种或多种技术的决定都针对大量约束进行了优化。毕竟,每个都是一种推测性优化,这意味着如果做得不好,它可能会触发不必要的操作和网络流量,甚至更糟,对用户触发的实际导航的加载时间产生负面影响。
Chrome 如何解决这个问题?预测器会消耗尽可能多的信号,包括用户生成的行动、历史浏览数据,以及来自渲染器和网络堆栈本身的信号。
与负责协调 Chrome 中所有网络活动的 ResourceDispatcherHost
类似,Predictor
对象在 Chrome 中创建了许多针对用户和网络生成的活动过滤器
ConnectInterceptor
对象,以便它可以观察流量模式并记录每个请求的成功指标例如,渲染进程可以使用以下任何提示触发消息给浏览器进程,这些提示方便地定义在 ResolutionMotivation
中 (url_info.h
)
enum ResolutionMotivation {
MOUSE_OVER_MOTIVATED, // Mouse-over initiated by the user.
OMNIBOX_MOTIVATED, // Omnibox suggested resolving this.
STARTUP_LIST_MOTIVATED, // This resource is on the top 10 startup list.
EARLY_LOAD_MOTIVATED, // In some cases we use the prefetcher to warm up
// the connection in advance of issuing the real
// request.
// The following involve predictive prefetching, triggered by a navigation.
// The referring_url_ is also set when these are used.
STATIC_REFERAL_MOTIVATED, // External database suggested this resolution.
LEARNED_REFERAL_MOTIVATED, // Prior navigation taught us this resolution.
SELF_REFERAL_MOTIVATED, // Guess about need for a second connection.
// <snip> ...
};
有了这样的信号,预测器的目标是评估其成功的可能性,然后在有资源的情况下触发活动。每个提示都可能有一个成功概率、一个优先级和一个过期时间戳,这些组合可以用来维护一个推测性优化的内部优先级队列。最后,对于从该队列中分派的每个请求,预测器也能够跟踪其成功率,这使它能够进一步优化其未来的决策。
牢记 Chrome 网络堆栈的 10,000 英尺架构视图,现在让我们更深入地了解浏览器中启用的面向用户的优化。具体来说,假设我们刚刚创建了一个新的 Chrome 配置文件,并准备开始我们的工作日。
你第一次加载浏览器时,它对你的收藏网站或导航模式知之甚少。但是,我们许多人都会在浏览器冷启动后遵循相同的例行公事,我们会导航到我们的电子邮件收件箱、收藏的新闻网站、社交网站、内部门户网站等等。具体网站会有所不同,但这些会话的相似性使 Chrome 预测器能够加速你的冷启动体验。
Chrome 会记住用户在浏览器启动后访问的十大最有可能的主机名——请注意,这不是全球十大目的地,而是专门指浏览器启动后访问的目的地。当浏览器加载时,Chrome 可以触发对这些可能目的地的 DNS 预取。如果你好奇,你可以通过打开一个新标签页并导航到 chrome://dns
来查看你自己的启动主机名列表。在页面顶部,你会找到你的个人资料的十大最有可能的启动候选人列表。
图 1.5 - 启动 DNS
图 1.5 中的截图是我自己 Chrome 个人资料的一个例子。我通常如何开始浏览?经常会导航到 Google 文档,如果我正在撰写像这样一篇文章的话。毫不奇怪,我们在列表中看到了很多 Google 主机名。
Chrome 的一项创新是引入了 Omnibox,与它的前身不同,它处理的不仅仅是目标 URL。除了记住用户以前访问过的页面的 URL 之外,它还提供对你历史记录的全文搜索,以及与你选择的搜索引擎的紧密集成。
当用户输入时,Omnibox 会自动提出一个操作,该操作要么是基于你导航历史记录的 URL,要么是搜索查询。在幕后,每个提议的操作都会根据查询及其过去的表现进行评分。事实上,Chrome 允许我们通过访问 chrome://predictors
来检查这些数据。
图 1.6 - Omnibox URL 预测
Chrome 会维护用户输入的前缀、它提出的操作以及每个操作的命中率的历史记录。对于我自己的个人资料,你可以看到,每当我输入“g”时,我前往 Gmail 的概率为 76%。一旦我添加一个“m”(表示“gm”),则置信度会上升到 99.8%——事实上,在记录的 412 次访问中,我只在输入“gm”一次后没有前往 Gmail。
这与网络堆栈有什么关系?可能候选人的黄色和绿色也为 ResourceDispatcher
提供了重要的信号。如果我们有一个可能的候选人(黄色),Chrome 可能会触发对目标主机的 DNS 预取。如果我们有一个高置信度候选人(绿色),则 Chrome 可能会在解析主机名后触发 TCP 预连接。最后,如果两者都在用户仍在思考时完成,则 Chrome 甚至可能在隐藏标签中预渲染整个页面。
或者,如果根据过去的导航历史记录,没有找到与输入的前缀匹配的良好候选人,则 Chrome 可能会发出 DNS 预取和 TCP 预连接以响应你的搜索提供商,以防出现可能的搜索请求。
普通用户需要数百毫秒才能填写他们的查询并评估提议的自动完成建议。在后台,Chrome 能够预取、预连接,在某些情况下甚至预渲染页面,因此,当用户准备好按“Enter”键时,大部分网络延迟已经消除。
最好、最快的请求是不发出的请求。每当我们谈论性能时,如果我们不谈论缓存,就会有所遗漏——你为网页上的所有资源提供了 Expires
、ETag
、Last-Modified
和 Cache-Control
响应头,对吧?如果没有,那就去修复它。我们会等你的。
Chrome 有两种不同的内部缓存实现:一种由本地磁盘支持,另一种将所有内容存储在内存中。内存实现用于 隐身浏览模式,并且会在你关闭窗口时被清空。两者都实现了相同的内部接口 (disk_cache::Backend
和 disk_cache::Entry
),这极大地简化了架构,而且如果你有兴趣,你可以轻松地尝试你自己的实验性缓存实现。
在内部,磁盘缓存实现了它自己的一组数据结构,所有这些数据结构都存储在你的个人资料的单个缓存文件夹中。在这个文件夹中,有索引文件,这些文件在浏览器启动时被内存映射,还有数据文件,这些文件存储实际数据以及 HTTP 头和其他簿记信息。4 最后,为了进行逐出,磁盘缓存维护一个最近最少使用 (LRU) 缓存,该缓存会考虑访问频率和资源年龄等排名指标。
如果您想了解 Chrome 缓存的状态,可以打开一个新标签页,并导航到chrome://net-internals/#httpCache
。或者,如果您想查看实际的HTTP 元数据和缓存的响应,也可以访问chrome://cache
,它会列出当前缓存中所有可用的资源。从该页面中,搜索您要查找的资源,然后点击URL 以查看确切的缓存标头和响应字节。
我们已经在很多场合提到过DNS 预解析,所以在我们深入探讨其实现之前,让我们回顾一下它可能被触发的场景以及原因。
在所有情况下,DNS 预解析都被视为提示。Chrome 并不保证预解析会发生,而是利用每个信号结合其自身的预测器来评估提示并决定采取的措施。在“最坏的情况下”,如果 Chrome 无法及时预解析主机名,用户将不得不等待显式DNS 解析,然后是TCP 连接时间,最后才是实际的资源获取。但是,当这种情况发生时,预测器可以记录下来并相应地调整其未来的决策——它会随着您的使用变得更快,更智能。
我们之前没有涉及的优化之一是 Chrome 能够学习每个网站的拓扑结构,然后使用这些信息来加速未来的访问。具体来说,请回想一下,一个平均页面包含 88 个资源,这些资源来自 15 个以上的不同主机。每次执行导航时,Chrome 可能会记录页面上流行资源的主机名,在未来的访问中,它可以选择触发DNS 预解析,甚至对其中一些或全部资源执行TCP 预连接。
要检查 Chrome 存储的子资源主机名,请导航到chrome://dns
并搜索您配置文件中任何流行的目标主机名。在上面的示例中,您可以看到 Chrome 为 Google+ 记住的六个子资源主机名,以及DNS 预解析触发次数或执行TCP 预连接次数的统计数据,以及预期每个主机将服务请求的次数。这种内部统计信息使 Chrome 预测器能够执行其优化。
除了所有内部信号外,网站所有者还可以将额外的标记嵌入到他们的页面中,以请求浏览器预解析主机名。
<link rel="dns-prefetch" href="//host_name_to_prefetch.com">
为什么不简单地依靠浏览器中的自动机制?在某些情况下,您可能希望预解析页面中未提及的任何主机名。重定向是典型的示例:链接可能指向一个主机(例如分析跟踪服务),然后将用户重定向到实际的目标地址。Chrome 本身无法推断出这种模式,但您可以通过提供手动提示来帮助它,并让浏览器提前解析实际目标地址的主机名。
这一切在幕后是如何实现的?这个问题的答案,就像我们已经讨论过的所有其他优化一样,取决于 Chrome 的版本,因为团队一直在尝试新的、更好的方法来提高性能。但是,总的来说,Chrome 内部的DNS 基础设施有两个主要实现。从历史上看,Chrome 依赖于与平台无关的getaddrinfo()
系统调用,并将实际查找的责任委托给操作系统。但是,这种方法正在被 Chrome 自行实现的异步DNS 解析器所取代。
依赖于操作系统的原始实现有其优势:代码更少更简单,并且能够利用操作系统的DNS 缓存。但是,getaddrinfo()
也是一个阻塞系统调用,这意味着 Chrome 必须创建和维护一个专用的工作线程池,以允许它并行执行多个查找。这个未加入的池被限制在六个工作线程,这是一个基于硬件的最低公分母的经验性数字——事实证明,更高的并行请求数量会导致某些用户的路由器过载。
对于使用工作线程池的预解析,Chrome 只是调度getaddrinfo()
调用,该调用会阻塞工作线程,直到响应准备就绪,然后它只是丢弃返回的结果并开始处理下一个预取请求。结果被OSDNS 缓存缓存,它对未来的实际getaddrinfo()
查找返回即时响应。它很简单,有效,在实践中也足够好。
嗯,有效,但还不够好。getaddrinfo()
调用隐藏了 Chrome 的很多有用信息,例如每个记录的生存时间(TTL)时间戳,以及DNS 缓存本身的状态。为了提高性能,Chrome 团队决定实现他们自己的跨平台异步DNS 解析器。
图 1.7 - 启用异步DNS 解析器
通过将DNS 解析移至 Chrome,新的异步解析器能够实现一些新的优化。
以上所有以及更多内容都是 Chrome 中持续试验和改进的想法。这带来了一个显而易见的问题:我们如何知道和衡量这些想法的影响?很简单,Chrome 会跟踪每个个人资料的详细网络性能统计数据和直方图。要检查收集的DNS 指标,请打开一个新标签页,然后前往chrome://histograms/DNS
(参见图 1.8)。
图 1.8 - DNS 预取直方图
上面的直方图显示了DNS 预取请求延迟的分布:大约 50%(最右边的列)的预取查询在 20 毫秒(最左边的列)内完成。请注意,这些数据基于最近的浏览会话(9869 个样本),并且对用户是私有的。如果用户选择在 Chrome 中报告其使用情况统计信息,那么这些数据的摘要将被匿名化,并定期向工程团队发出信号,然后工程团队能够看到其实验的影响并相应地进行调整。
我们已经预解析了主机名,并且根据 Omnibox 或 Chrome 预测器估计,一个可能性很高的导航事件即将发生。为什么不更进一步,在用户发出请求之前,也推测性地预连接到目标主机并完成TCP 握手?通过这样做,我们可以消除另一个完整的往返延迟,这可以轻松地为用户节省数百毫秒。嗯,这正是TCP 预连接的本质以及它的工作原理。
要查看已触发TCP 预连接的主机,请打开一个新标签页并访问chrome://dns
。
图 1.9 - 显示已触发TCP 预连接的主机
首先,Chrome 检查其套接字池,以查看是否有可用于主机名的可用套接字,它可能能够重用这些套接字——保持活动套接字会在池中保留一段时间,以避免TCP 握手和慢启动惩罚。如果没有可用的套接字,那么它可以启动TCP 握手,并将其放入池中。然后,当用户启动导航时,HTTP 请求可以立即发出。
对于好奇的人,Chrome 在chrome://net-internals#sockets
提供了一个实用程序,用于探索 Chrome 中所有打开的套接字的状态。图 1.10显示了一个屏幕截图。
图 1.10 - 打开的套接字
请注意,您还可以深入研究每个套接字并检查时间线:连接和代理时间、每个数据包的到达时间等等。最后但并非最不重要的一点是,您还可以导出这些数据以供进一步分析或错误报告。拥有良好的检测工具是任何性能优化的关键,而chrome://net-internals
是 Chrome 中所有网络相关事物的核心——如果您还没有探索它,您应该去探索它!
有时,页面的作者能够根据其网站的结构或布局提供额外的导航或页面上下文,并帮助浏览器优化用户的体验。Chrome 支持两种此类提示,它们可以嵌入到页面的标记中。
<link rel="subresource" href="/javascript/myapp.js">
<link rel="prefetch" href="/static/big.jpeg">
子资源和预取看起来非常相似,但语义却大不相同。当一个链接资源将其关系指定为“预取”时,它表示浏览器可能会在将来的导航中需要此资源。换句话说,它实际上是一个跨页面的提示。相反,当一个资源将其关系指定为“子资源”时,它表示浏览器将在当前页面上使用此资源,并且它可能希望在稍后在文档中遇到它之前发出请求。
正如您所预期的那样,提示的不同语义会导致资源加载器表现出截然不同的行为。标记为预取的资源被认为是低优先级的,并且只有在当前页面完成加载后,浏览器才可能下载这些资源。子资源是在遇到它们时以高优先级获取的,并且将与当前页面上的其他资源竞争。
这两个提示,如果使用得当并在正确的上下文中使用,可以帮助您显着优化网站上的用户体验。最后,还必须注意的是,预取是HTML5 规范的一部分,并且截至今天,Firefox 和 Chrome 都支持它,而子资源目前仅在 Chrome 中可用。
不幸的是,并非所有网站所有者都能够或愿意在其标记中为浏览器提供子资源提示。此外,即使他们提供,我们也必须等到从服务器接收HTML 文档,才能解析提示并开始获取必要的子资源——这取决于服务器响应时间,以及客户端和服务器之间的延迟,这可能需要数百甚至数千毫秒。
但是,正如我们之前看到的那样,Chrome 已经在学习流行资源的主机名以执行DNS 预取。那么,为什么它不能做到这一点,而是更进一步,执行DNS 查找,使用TCP 预连接,然后也推测性地预取资源?嗯,这正是预取的功能。
资源预刷新是 Chrome 中每个实验性优化的工作流程的一个很好的例子 - 从理论上讲,它应该能够提高性能,但也有许多权衡。只有一个方法可以可靠地确定它是否能成功并进入 Chrome:实施它并作为 A/B 实验在一些预发布通道中使用真实用户、真实网络和真实浏览模式运行它。
截至 2013 年初,Chrome 团队正处于讨论实施的早期阶段。如果根据收集的结果,它能够成功,我们可能会在今年晚些时候在 Chrome 中看到预刷新。改进 Chrome 网络性能的过程从未停止 - 团队一直在尝试新的方法、想法和技术。
到目前为止,我们已经介绍的每一个优化都有助于减少用户直接请求导航与结果页面在标签页中渲染之间的延迟。但是,要获得真正即时的体验需要做什么?根据我们之前看到的 UX 数据,这种交互必须在不到 100 毫秒的时间内完成,这几乎没有留下网络延迟的余地。我们可以做些什么才能在不到 100 毫秒的时间内提供一个已渲染的页面?
当然,你已经知道答案了,因为这是一种许多用户常用的模式:如果你打开了多个标签页,那么在标签页之间切换是即时的,并且肯定比在单个前台标签页中等待相同资源之间的导航要快得多。那么,如果浏览器提供了一个 API 来执行此操作呢?
<link rel="prerender" href="http://example.org/index.html">
你猜对了,那就是 Chrome 中的预渲染。与“prefetch”提示只下载单个资源不同,“prerender”属性指示 Chrome 在一个隐藏的标签页中预渲染页面及其所有子资源。隐藏的标签页本身对用户是不可见的,但当用户触发导航时,该标签页将从后台切换到前台,从而获得“即时体验”。
想尝试一下吗?你可以访问 prerender-test.appspot.com 进行动手演示,并通过访问以下地址查看你的配置文件的预渲染页面的历史记录和状态:chrome://net-internals/#prerender
。(参见 图 1.11。)
图 1.11 - 当前配置文件的预渲染页面
正如你所料,在隐藏的标签页中渲染整个页面会消耗大量的资源,包括 CPU 和网络,因此只有在我们有充分的信心会使用隐藏的标签页时才能使用它。例如,当你使用 Omnibox 时,可能会为高置信度建议触发预渲染。类似地,如果 Google 搜索估计第一个搜索结果是高置信度目的地(也称为 Google 即时页面),它有时会在其标记中添加预渲染提示。
请注意,你也可以将预渲染提示添加到你自己的网站中。在你这样做之前,请注意,预渲染有一些限制和局限性,你应该牢记这些限制和局限性。
换句话说,预渲染不能保证发生,并且只适用于安全的页面。此外,由于 JavaScript 和其他逻辑可能在隐藏的页面中执行,因此最好利用 页面可见性 API 来检测页面是否可见 - 这本来就是你 应该做的事情。
不用说,Chrome 的网络堆栈远不止一个简单的套接字管理器。我们 whirlwind 浏览涵盖了许多潜在的优化级别,这些优化在你浏览网页时在后台透明地执行。Chrome 对网络拓扑和你的浏览模式了解得越多,它就能更好地完成工作。几乎就像魔法一样,Chrome 在你使用时变得更快。但它不是魔法,因为你现在知道它是怎么工作的。
最后,需要注意的是,Chrome 团队一直在不断迭代和尝试新的想法以提高性能 - 这一过程从未停止。当你阅读本文时,很可能会有新的实验和优化正在开发、测试或部署。也许一旦我们实现了每个页面的即时页面加载目标(< 100 毫秒),我们就可以休息一下。在那之前,还有很多工作要做。