Google Chrome 中的高性能网络技术实践(翻译)下

你使用浏览器进行会话的全过程

通过前文我们已经能在心中绘制出Chrome网络栈的大致图样,现在让我们来详细了解一下浏览器中一系列面向用户的优化。现在,让我们想想我们刚刚创建了一个Chrome的用户档案,并准备开始上网冲浪了。

优化游览器的冷启动体验

pagespeed

第一次启动Chrome的时候,它对你喜爱的网站、浏览习惯还了解甚少。但是,大多数人在浏览器冷启动后还是遵循着一个特定的使用习惯,比如打开电邮、新闻头条、社交网络和门户网站等等。具体的网站可能不同,但是它们其中的相似性可以帮助预测器来加速你每天一开始打开浏览器的体验!

Chrome总是记着用户打开浏览器后最爱去的十大网站,不过需要注意的是这并不是全局浏览的前十,而仅仅是启动后的。浏览器加载的时候,Chrome就可以为这些站点发起DNS的预加载。好奇的你可以通过chrome://dns来查看你自己的启动域名列表。在这个网页的最上方,你会找到你账号的十大启动候选网站。

startup-dns

以我账号举个栗子吧,我一般如何上网的呢?如果我是在写一篇文章的时候,我会直接导航去Google Docs。如你所料,我常去一些Google站点。

优化与Omnibox的互动

chrome-omnibox

推出Omnibox是Chrome的一大创举,和先前的其他浏览器相比,Chrome的Omnibox可不仅仅只能输入目标URL。除了记住用户之前浏览过的URL以外,它也支持历史浏览记录的全文搜索,(小技巧:输入网站的名称而不是URL),更可与你选择搜索引擎进行深度整合。

Omnibox会自动相应用户的输入,这可以是根据你历史的一个URL,或者是一个搜索查询。底层实现中,每个提示的动作都是按照和输入的相关程度进行排序,并参考之前的记录。事实上,你可以在chrome://predictors中查询到这些数据。

xomnibox

Chrome记忆着用户输入前缀词的一份历史记录,其提示的动作和成功执行的比例。对于我的历史来说,可以发现我每次敲入g后,我有76%的机率是向打开Gmail。而当我再输入”m”后(“gm”的缩写),这一动作的可信率升到了99.8%。事实上,在412次我输入”gm”后,我只有一次并不是想打开Gmail。

但是,你可能会想,这一切关网络栈什么事情?这些黄黄绿绿的候选数据其实都是ResourceDispatcher的重要参考信号!如果我们产生了一个黄色信号,Chrome可能会发出一个DNS预查询。如果是一个绿色的更高可信度的信号,Chrome可能在DNS解析后发出一个TCP预连接。如果这些都做好了,但用户还没有做出决定,Chrome甚至会默默在后台页面渲染好这个网页。

还有一种可能是,如果就历史数据来看,当前输入的查询词没有很好的匹配,那么Chrome会对你搜索引擎进行DNS/TCP预加载,这是预测你很有可能发出一个搜索请求。

对于一般用户来说,他们需要数百毫秒的时间来输入查询,并评估自动弹出的提示。在后台的Chrome便能从容地预加载、预连接甚至于在某些情况下预渲染这个网页,这样等用户敲击下”enter”键的时候,网络带来的延时影响已经没有了。

优化缓存的性能

最快的Request,是这个请求根本没有发生。当我们谈及性能的时候,怎能不谈及缓存呢。作为网站开发人员,你得通过Expires,ETag,Last-Modified和Cache-Control这些response headers来表明你服务器上资源文件的缓存状态,对吧?如果没有的话,快行动起来,我们等等没关系。

chrome-incognito

Chrome对于内部缓存有两种不同的实现:一种是本地磁盘缓存,一种使用内存。内存缓存是为incognito浏览模式准备的,当你关闭窗口后,一切干干净净。两种模式都实现了相同的接口(disk_cache::Backenddisk_cache::Entry),这极大简化了架构设计。如果你对此有想法,可以很轻易地实验你的缓存实现。

从内部来说,磁盘缓存实现了自有的一套数据结构,它们都存放在你Profile目录下的文件夹中。其中,索引文件在浏览器启动时被直接映射入内存,而数据文件存放真实的数据,比如说HTTP头和其他统计信息。这里值得一提的是,最大不超过16KB的文件都存放在共享的数据块文件中,大文件直接存放在专有文件中。磁盘缓存需要实验LRU测量进行清扫,LRU的通常参考使用频率和资源的年龄。

internals-cache

如果你对Chrome缓存感兴趣的话,不妨看看chrome://net-internals/#httoCache。如果你想查看实际的HTTP元数据和缓存的Response,你可以访问chrome://cache,它们会列出缓存中所有的资源详情。你可以搜索,并点击URl查看。

使用预加载优化DNS

之前我们已经几次提到了DNS预解析,那在我们展开实现细节前,我们看看哪些情况下会触发DNS解析,和触发理由:

  • 在Render进程中运行的Blink文档解析器,可以提供其页面的所有URL中的域名,Chrome可以选择预先解析与否。
  • 在用户发出请求之前,Render进程便可能触发一个鼠标悬浮事件,或是一个按钮点击事件。
  • 在高匹配的情况下,Omnibox可能触发一个查询请求。
  • Chrome预测器在综合历史浏览记录和资源请求数据后,发出域名解析请求。
  • 页面的开发者可以显式告诉Chrome,哪些域名可以预先解析。

在所有以上的情况下,DNS预解析都只是被当做暗示来处理。Chrome不保证预解析一定会执行,而是通过综合判断所有的信号,使用预测器来判断是否执行。最“糟糕”的情况下,如果我们不能提前解析DNS,用户就必须要等待一个DNS解析时间,然后才是TCP连接和资源加载。不过,如果这一切发生的话,预测器会进行记录并以此优化未来的决策。这就是你越用,Chrome越快越聪明。

之前我们没有提到的一个优化是,Chrome会发现每个网站的拓扑结构,并用这一信息为未来的浏览加速。让我们回忆一下每个网页平均由88个资源的事实,其中资源从30+个独立的域名加载而来。这说明你每次进行网页浏览的时候,Chrome可以记录下网页上流行的资源文件。在以后它便可选择为一些、或者是全部发出一个DNS预加载和TCP预连接。

xsubresource-stats

你可访问chrome://dns来查看Chrome缓存的子站点的域名,并可以查询一下你关心的域名。上例可看出,对于Google+来说,Chrome记得六个子站点和DNS预解析/TCP预连接的统计数据,其中还有请求的期望值。这一内部的统计数据让Chrome预测器可以进行预测优化。

除了上述的所有内部信号,站点开发者还可以通过以下方式,即嵌入附加的预解析信息来暗示Chrome:

1
2
3
4

<link rel="dns-prefetch" href="//host_name_to_prefetch.com">

```

那让浏览器自动来做不就好了吗?在某些情况下,可能有预解析一个文章中完全没有提及的域名。最经典的栗子当然就是跳转了:一个指向域名的链接,就比如一个追踪分析的服务,它功能就是将用户跳转到真实的地址下。Chrome仅仅依靠自己是没有办法做到的,而可以通过人工提供更好。

那么底层实现又是怎样的呢?不过这一答案恐怕和Chrome的版本有关,由于团队总是在实验更新更好的方法。不过整体上来看,Chrome的DNS组织结构有两大实现:之前Chrome使用平台无关的getaddrinfo系统调用,将这个查询全权交由系统进行,而现在逐步替换为Chrome自实现的异步DNS解析器

原本的实现的优点是,由于依赖操作系统,代码可以更简洁,并且还能利用上操作系统的DNS缓存。不过,getaddrinfo()是一个阻塞式的系统调用,这就意味着Chrome得创建并维护一个专有的线程池来进行并行的解析。这个线程池最大不超过6个worker线程,这是基于硬件最小公共分母的一个经验值,因为我们发现太高的并行请求会让一些用户的路由器过载!

对于使用worker池的预解析方案,Chrome就简单地调用getaddrinfo(),这会阻塞到response返回,在此之后他就丢弃了返回的结果并转而处理下一个请求。丢弃它?由于结果已经由系统DNS守护进程缓存了,这以后就能立即返回了。这足够简单和有效。

嗯,很有效,但这还远远不够!getaddrinfo()调用并不会告诉Chrome很多有用的信息,比如说每个记录的TTL,和DNS缓存自身的状态。为了提升性能,Chrome团队决定实现一个跨平台、异步的DNS解析器。

xasync-dns

Chrome自己进行DNS解析带来了以下的新优化:

  • 对于重传输计时器更好的控制,并且能并行多个查询
  • TTL信息可见,这让Chrome可以在失效之前就更新热门的记录
  • 对于IPv4和IPv6更好的支持
  • 基于RTT和其他信号转换去其他的服务器(DNS服务器)

以上,乃至还有更多,都是来源于Chrome持续不断的实验和优化。一个更加明显的问题是:我们是如何验证这些想法的效果的呢?,这很简单,Chrome对于每个用户,都对网络性能进行了细致的追踪和记录。你可以打开chrome://histograms/DNS来查看这些信息。

xdns-prefetch

上图显示了DNS预请求的时间延时分布情况:差不多半成的(右栏)都是在20毫秒内完成的(左栏)。这是基于最近的一次浏览回话(9869条记录)并为用户私有。如果用户选择向Chrome提交他们的使用情况数据的话,这些数据才会被匿名化后,周期性地提交到开发团队,这样我们就能看到试验成功并不断做出调整了。就这样反复迭代更新。

使用预连接来优化管理TCP连接

既然我们通过Omnibox和Chrome预测器猜测出了接下来用户的导航方向,并预先解析出了域名,那为什么不再提前一步,预先与目标域名建立TCP连接,在用户发出真实的请求前就完成TCP握手呢?这样的话,我们便又节省了一个完整的Roundtrip时延,为用户节省了数百毫秒的延时。这便是TCP预连接和它的工作原理了。

首先,Chrome会先检查其socket池,找找有没有此域名的可用的socket。重用已经保持一段时间的keep-alive的sockets可以防止TCP握手和慢启动惩罚。如果没有可用的socket,那再启动TCP握手,并将它放到池中。这样的话,当用户启动导航的时候,真实的HTTP请求就能立即发送出去了。

打开chrome://net-internals#sockets来查看Chrome中打开的socket详情统计。

xnetinternals-sockets

你还可以深入每个socket的详情并审查时间线:连接和代理时间,每个包的到达时间等等。你还可以将数据导出,用于离线分析和bug报告。一个好审视(instrumentation)系统的是性能优化的关键,而chrome://net-internals是Chrome网络的汇总点,如果你还没尝试它的话快去试试吧。

使用预加载提升来优化资源加载

有时,网页开发者基于站点的布局信息,可以提供附加的导航信息,或是page context(不知原文所指)。这能帮助浏览器优化用户体验。Chrome支持以下两种提示,内嵌在网页中即可:

1
2
3
4
5

<link rel="subresource" href="/javascript/myapp.js">
<link rel="prefetch" href="/images/big.jpeg">

```

子资源和预加载看起来没有什么不同,但却有着非常不同的语义。当一个链接声明自己与当前网页的关系是”prefetch”的时候,这只是暗示浏览器它是一个可能在未来需要打开的页面。换句话说,它只是一个跨页面提示。对比来看,”subresource”提示浏览器本资源是可能会被用于当前页面的,这需要在遇到这个资源之前发出request。

如你所料,不同的语义也带来了资源加载器非常不同的行为。标注为prefetch的资源只是被赋予低优先级,只有在本页面结束加载后才会考虑。而subresource资源则一遇到就加载,优先级很高。

这两个提示,如果在合适的环境下正确使用,可以显著地优化你站点的用户体验。最后,要提示大家prefetch是HTML5标准的一部分,现已经获得了Firefox和Chrome的支持,而subresource只是在Chrome中支持。(此信息可能已经过期)

使用浏览器预刷新来优化资源加载

不幸的是,不是所有的站点开发人员都可以或是愿意在网页标注出subresource。就算他们这么做了,我们必须等待HTML文档返回,才能解析到这些提示,并开始获取必要的subresource。这就要看服务器的响应时间,和延时了。通常数百甚至上千的延时都有可能出现。

但是,让我们眼光再放得长远一些,Chrome已经知道了热门资源的域名,并以此进行DNS的预解析。为什么我们不再向前推进一步,进行DNS查询,使用TCP预连接,并推测性地预先加载资源呢?这便是”预刷新”想做的。

  • 用户打开目标URL
  • Chrome查询预测器,得到和目标URL相关的子资源,并也开始DNS预解析-TCP预连接-资源预刷新
  • 如果子资源已经缓存,那么只要从磁盘或是内存中加载就好
  • 如果资源缺失,或是缓存到期,那么才发出网络请求

xchrome-experiment

资源预刷新是Chrome中性能优化的一个典型范例,理论上来说,它应该能带来更好的性能,不过这其中还是有很多折衷。只要一种方式来选择是否需要将这个功能融合入Chrome,就是实现它并在pre-release的Chrome中给实际用户、实际的网络和实际的浏览中进行A/B测试。

在2013年早期,Chrome团队就已经在讨论这一实现。如果收集的反馈好,我们就可以在2013年后期看到这一功能。提升Chrome网络性能的脚步从未停止,团队一直在尝试新的方法、想法和技术

使用预渲染优化浏览体验

我们以上提及的每一个优化都减少了用户真实请求发出的时间,最终让页面得以渲染。但是,一个真实的秒开的体验还需要什么呢?基于我们之前预先看到的UX数据,互动必须在100ms中结束,这便不给网络延时留多少空余时间了,我们要怎样才能在100ms中渲染好一个页面呢?

当然,你应该已经猜到了答案。这通常是大多数人的习惯:如果你打开了很多网页,并在其中切换,这就是秒开的体验,可比等待加载快多了。如果浏览器已经为你提供了API来这么做呢?

1
2
3
4

<link rel="prerender" href="http://example.org/index.html">

```

你猜对了,这边是Chrome中的预渲染。不像”prefetch”只是下载一个资源,”prerender”提示Chrome在加载其所有subresource后,在一个隐藏的标签页中渲染这个页面。隐藏的tab是不为用户所见的,但当用户触发导航的时候,此tab就被从后台交换出来,形成所谓的”秒开”

你可以访问prerender-test.appspot.com来体验一下,打开chrome://net-internals/#prerender来查看预渲染的历史和当前状态。

xnetinternals-prerender.png

如你所料,在后台tab中完整地渲染一个页面需要消耗大量的CPU和网络资源,因此我们只会在此页面非常可能用到的时候才会进行预渲染!比如之前提到的Omnibox的高可能性提示。相似的是,Google搜索有时也会为第一结果加入预渲染的提示(也称谓Google Instant Pages)。

视频地址

你也可以为你的网站添加预渲染的提示。不过,在你这么做之前,你需要知道预渲染由一些限制:

  • 在所有进程中,只允许一个预渲染的tab
  • 不支持HTTPS和需要认证的HTTP页面
  • 如果请求的资源,或其任何subresource需要进行一个非幂等(原文:nonidempotent)请求,(只允许GET)
  • 所有的资源都是以最低网络优先级发出的
  • 所有的页面都是以最低CPU优先级渲染的
  • 如果页面使用超出100MB的内存,就会终止
  • 推迟插件的初始化,如果存在HTML 5 媒体元素的话,也会终止

换句话说,不能保证预渲染一定进行,并只在安全的情况下进行。还需提到的是,隐藏的页面也会执行Javascript和其他逻辑,最佳实践是利用Page Visibility API来检查页面是否可见,这也是你一定会学的

Chrome越用越快

xchrome-speed-final

现在看来,Chrome的网络栈可比一个朴素的socket管理器复杂多了。我们这个简短地介绍了在浏览网页时你不可见的很多优化。Chrome越是学习网络的拓扑和你的习惯,做得越好。这就好像魔术一样,Chrome是越用越快的,不过它不是魔法,因为你已经知道它的内部机理了

最后,还是需要提到Chrome团队一直努力迭代和实验,来不断提高性能。在你读到这里的时候,还是由很多新实验和优化在开发、测试和部署中。兴许某一天,我们达到了我们秒开(小于100ms)的目标时,我们可以歇一歇脚,不过在那之前,还有很多路要走!

Ilya Grigorik,Google Web性能工程师,W3C Web性能小组的co-chair,High Performance Browser Networking (O’Reilly) 一书的作者,Twitter,Google+