将 Service Worker 引入 Google 搜索

关于已发货、如何衡量影响以及做出的取舍的故事。

背景

在 Google 上搜索任意主题时,您就会看到一个易于辨认的页面,其中包含有意义且相关的结果。您可能还没有意识到,在某些情况下,此搜索结果页是通过一项名为 Service Worker 的强大网络技术提供服务的。

若要在不对性能产生负面影响的情况下推出针对 Google 搜索的 Service Worker 支持,需要跨多个团队工作数十名工程师。下面将介绍具体开发项目、如何衡量性能以及做出哪些权衡。

探索 Service Worker 的主要原因

向 Web 应用添加 Service Worker 就像对网站进行任何架构更改一样,在完成时应牢记一组明确的目标。对于 Google 搜索团队而言,添加 Service Worker 有几个主要原因值得深入研究。

有限的搜索结果缓存

Google 搜索团队发现,用户在短时间内多次搜索相同的字词是很常见的。搜索团队希望利用缓存并在本地完成这些重复的请求,而不是仅仅为了获取可能相同的结果而触发新的后端请求。

新鲜度的重要性无法忽视,有时用户会反复搜索相同的字词,因为这是一个不断演变的主题,他们期望看到最新结果。使用 Service Worker 可让搜索团队实现精细的逻辑来控制本地缓存的搜索结果的生命周期,并在速度与新鲜度之间实现精确平衡,即他们认为最能满足用户的需求。

有意义的离线体验

此外,Google 搜索团队还希望提供有意义的离线体验。当用户想要了解某个主题时,会直接前往 Google 搜索页面并开始搜索,而无需担心有效的互联网连接。

如果没有 Service Worker,在离线状态下访问 Google 搜索页面只会转到浏览器的标准网络错误页面,用户必须记得返回,并在连接返回后重试。借助 Service Worker,您可以提供自定义离线 HTML 响应,并允许用户立即输入搜索查询。

后台重试界面的屏幕截图。

在连接到互联网之前无法获得结果,但 Service Worker 允许使用后台同步 API 将搜索延迟并立即发送到 Google 的服务器。

更智能的 JavaScript 缓存和传送

另一个动机是优化模块化 JavaScript 代码的缓存和加载,这些代码可为搜索结果页上的各种功能提供支持。JavaScript 捆绑提供的很多优势在没有 Service Worker 参与的情况下很有必要,因此搜索团队并不想完全停止捆绑。

通过使用 Service Worker 在运行时对精细的 JavaScript 块进行版本控制和缓存的功能,搜索团队怀疑他们能够减少缓存抖动量,并确保将来重复使用的 JavaScript 能够高效缓存。其 Service Worker 中的逻辑可以分析包含多个 JavaScript 模块的软件包的传出 HTTP 请求,并通过将多个本地缓存的模块组合在一起(尽可能有效地“拆分”)来执行该请求。这可以节省用户带宽,并提高整体响应速度。

使用由 Service Worker 提供的缓存 JavaScript 还有性能方面的优势:在 Chrome 中,系统会存储和重复使用该 JavaScript 的经过解析的字节码表示形式,从而减少在运行时为在网页上执行 JavaScript 而需要完成的工作。

挑战和解决方案

以下是实现团队既定目标需要克服的一些障碍。虽然其中一些挑战是 Google 搜索特有的,但许多挑战适用于可能考虑部署 Service Worker 的各种网站。

问题:Service Worker 开销

在 Google 搜索上启动 Service Worker 的最大挑战也是真正的阻碍,那就是确保它不会执行任何可能会增加用户感知的延迟时间的操作。Google 搜索非常重视性能,而且过去即使对于给定用户群,即使新功能导致的延迟时间延长了几十毫秒,它也会阻止发布新功能。

当该团队在最早的实验中开始收集性能数据时,问题就明显了。为了响应针对搜索结果页的导航请求而返回的 HTML 是动态的,会根据需要在 Google 搜索的网络服务器上运行的逻辑而出现很大差异。目前,Service Worker 无法复制此逻辑并立即返回缓存的 HTML。它只能将导航请求传递给后端网络服务器,这需要发出网络请求。

如果没有 Service Worker,则此网络请求会在用户导航后立即发生。注册 Service Worker 后,系统始终需要启动它并有机会执行其 fetch 事件处理脚本,即使这些提取处理程序除了进入网络之外没有机会执行任何其他操作时也是如此。启动和运行 Service Worker 代码所花费的时间是每次导航之上增加的纯开销:

示意图:SW 启动阻止导航请求。

这会给 Service Worker 实现带来太多的延迟,不利于证明任何其他优势。此外,该团队还发现,根据实际设备上的 Service Worker 启动时间的测量结果,启动时间分布很广,其中一些低端移动设备启动 Service Worker 所需的时间几乎与向结果页面的 HTML 发出网络请求所需的时间一样。

解决方案:使用导航预加载

让 Google 搜索团队能够继续推进其 Service Worker 启动的一项最重要的功能是导航预加载。对于需要使用网络响应来满足导航请求的 Service Worker,使用导航预加载是一项关键的性能优势。它会在 Service Worker 启动的同时,提示浏览器立即开始发出导航请求:

示意图:与导航请求并行完成的 SW 启动。

只要 Service Worker 启动所需的时间少于从网络收到响应所需的时间,就应该不会产生任何 Service Worker 带来的任何延迟开销。

此外,搜索团队还需要避免在低端移动设备上使用 Service Worker,因为这类设备上的 Service Worker 启动时间可能会超过导航请求时间。由于“低端”设备的具体构成没有硬性规定,因此他们想出了检查设备上安装的总 RAM 的启发法。内存不足 2 GB 的所有内存都归入其低端设备类别,在这些类别中,Service Worker 的启动时间无法接受。

另一个因素是可用存储空间,因为要缓存以供将来使用的全部资源可以运行几兆字节。借助 navigator.storage 接口,Google 搜索页面可以提前了解其缓存数据的尝试是否会因存储配额失败而失败。

这为搜索团队提供了多项条件,他们可以利用这些条件来确定是否使用 Service Worker:如果用户使用支持导航预加载的浏览器访问 Google 搜索页面,并且至少有 2 GB RAM 和足够的可用存储空间,则注册 Service Worker。不符合该条件的浏览器或设备最终不会启用 Service Worker,但它们仍会获得一如既往的 Google 搜索体验。

这种选择性注册的一个附带好处是能够发送更小、更高效的 Service Worker。使用相当新型的浏览器运行 Service Worker 代码可以消除旧版浏览器进行转译和 polyfill 的开销。这最终从 Service Worker 实现的总大小中缩减了大约 8 千字节的未压缩 JavaScript 代码。

问题:Service Worker 范围

在搜索团队运行了足够的延迟时间实验,并且确信导航预加载为他们提供使用 Service Worker 的可行且不依赖于延迟时间的路径后,一些实际问题开始浮出水面。其中一个问题与 Service Worker 的范围限定规则有关。Service Worker 的作用域决定了它可以控制哪些页面。

限定范围基于网址路径前缀。对于托管单个 Web 应用的网域,这没有问题,因为您通常仅使用具有 / 最大作用域的 Service Worker,该 Service Worker 可以控制该网域下的任何页面。不过,Google 搜索的网址结构稍微复杂一点。

如果 Service Worker 被赋予 / 的最大范围,它最终将能够控制在 www.google.com(或等效区域)下托管的任何网页,并且该网域下存在与 Google 搜索无关的网址。更合理、更严格的范围是 /search,它至少可以排除与搜索结果完全无关的网址。

遗憾的是,即使 /search 网址路径在 Google 搜索结果的不同形式之间共享,并且网址查询参数将决定显示哪种类型的搜索结果。其中一些变种使用的代码库与传统网页搜索结果页完全不同。例如,Image Search 和 Shopping Search 都采用不同的查询参数在 /search 网址路径下提供,但这两个界面(目前)都无法提供自己的 Service Worker 体验。

解决方案:创建调度和路由框架

虽然一些提案允许使用比网址路径前缀更强大的功能来确定 Service Worker 范围,但 Google 搜索团队在部署一个 Service Worker 时却无法对其控制的部分页面执行任何操作。

为了解决此问题,Google 搜索团队构建了定制的调度和路由框架,可以对其进行配置以检查客户端页面的查询参数等条件,并使用这些条件确定要查找的具体代码路径。该系统不是硬编码规则,而是具有灵活性,并且允许共享网址空间(如图片搜索和购物搜索)的团队在决定实现自己的 Service Worker 逻辑时,随时加入自己的 Service Worker 逻辑。

问题:个性化结果和指标

用户可以使用自己的 Google 帐号登录 Google 搜索,并且系统可能会根据其特定的帐号数据自定义搜索结果体验。登录的用户可通过特定的浏览器 Cookie 进行标识。浏览器 Cookie 是一项久负盛名且受到广泛支持的标准。

但使用浏览器 Cookie 的一个缺点是,它们不会在 Service Worker 内公开,并且无法自动检查它们的值并确保它们不会因用户退出或切换帐号而发生变化。(我们正在努力为 Service Worker 引入 Cookie 访问权限,但在撰写本文时,这种方法属于实验性方法,并未得到广泛支持。)

Service Worker 的当前登录用户视图与登录 Google 搜索网页界面的实际用户不匹配,可能会导致搜索结果个性化不正确或指标和日志记录归因错误。任何此类失败情况都会对 Google 搜索团队来说都是严重问题。

解决方案:使用 postMessage 发送 Cookie

Google 搜索团队采取了一种临时解决方案,而不是等待实验性 API 启动并提供对 Service Worker 内浏览器 Cookie 的直接访问:每当加载由 Service Worker 控制的网页时,该网页都会读取相关 Cookie 并使用 postMessage() 将这些 Cookie 发送到 Service Worker。

然后,Service Worker 会对照预期值检查当前的 Cookie 值,如果不匹配,则会采取措施从其存储空间中完全清除任何特定于用户的数据,然后重新加载搜索结果页,而不会出现任何错误的个性化。

Service Worker 为将内容重置为基准所采取的具体步骤取决于 Google 搜索的要求,但相同的常规方法对于处理脱离浏览器 Cookie 的个性化数据的其他开发者来说可能很有用。

问题:实验和活力

如前所述,Google 搜索团队高度依赖于在生产环境中运行实验,以及在默认启用新代码和新功能之前在现实环境中测试它们的效果。对于严重依赖缓存数据的静态 Service Worker,这可能有点难度,因为选择用户加入和退出实验通常需要与后端服务器进行通信。

解决方案:动态生成的 Service Worker 脚本

该团队采用的解决方案是使用动态生成的 Service Worker 脚本(由 Web 服务器为每个用户自定义),而不是使用提前生成的单个静态 Service Worker 脚本。此自定义 Service Worker 脚本中直接包含有关可能影响 Service Worker 行为或网络请求的实验的信息。更改用户的活跃体验集是通过结合使用传统技术(如浏览器 Cookie)以及在已注册的 Service Worker 网址中提供更新代码来完成的。

在极少数情况下,如果 Service Worker 实现出现需要避免的严重 bug,使用动态生成的 Service Worker 脚本还可以让您更轻松地提供应急方法。动态服务器工作器响应可能是一种空操作实现,可有效为部分或全部当前用户停用 Service Worker。

问题:协调更新

任何实际的 Service Worker 部署面临的最棘手的挑战之一是,在避免网络有利于缓存与同时确保现有用户在部署到生产环境后很快获得关键更新和更改之间做出合理权衡。适当的平衡取决于多种因素:

  • 您的 Web 应用是否为长期存在的单页应用,用户无需前往新页面即可无限期地保持打开状态。
  • 后端 Web 服务器的更新部署节奏是怎样的。
  • 普通用户是否能够容忍使用版本略过时的 Web 应用版本,或者新鲜度是否是首要任务。

在对 Service Worker 进行实验时,Google 搜索团队确保让实验在一系列预定的后端更新中保持运行,以确保指标和用户体验更接近用户在现实世界中最终看到的指标和用户体验。

解决方案:平衡新鲜度和缓存利用率

在测试了许多不同的配置选项后,Google 搜索团队发现以下设置在新鲜度和缓存利用率之间实现了适当的平衡。

Service Worker 脚本网址使用 Cache-Control: private, max-age=1500(1500 秒,即 25 分钟)响应标头提供,并在注册时将 updateViaCache 设置为“all”以确保标头有效。正如您能想象的那样,Google 搜索网页后端是一个庞大的全球分布服务器集,需要尽可能接近 100% 的正常运行时间。这项更改以滚动方式部署会影响 Service Worker 脚本内容的更改。

如果用户点击一个已更新的后端,然后快速导航到另一个页面,而后者遇到尚未收到更新 Service Worker 的后端,则他们最终会在版本之间多次翻转。因此,告知浏览器仅在自上次检查后 25 分钟后才检查更新后的脚本,这不会带来很大的负面影响。选择启用此行为的好处在于,可显著减少动态生成 Service Worker 脚本的端点所接收的流量。

此外,系统会在 Service Worker 脚本的 HTTP 响应中设置 Dialogflow 标头,以确保在 25 分钟后进行更新检查时,如果在此期间部署的 Service Worker 未出现任何更新,服务器可以通过 HTTP 304 响应进行高效响应。

虽然 Google 搜索 Web 应用中的某些互动会使用单页应用样式的导航(即通过 History API),但在大多数情况下,Google 搜索是一款使用“真实”导航的传统 Web 应用。当团队决定使用以下两个选项加快 Service Worker 更新生命周期的效率时,这一点正好可以发挥作用:clients.claim()skipWaiting()。点击 Google 搜索界面通常最终会转到新的 HTML 文档。调用 skipWaiting 可确保更新后的 Service Worker 在安装后立即有机会处理这些新的导航请求。同样,调用 clients.claim() 意味着更新后的 Service Worker 在 Service Worker 激活后有机会开始控制任何不受控制的已打开 Google 搜索页面。

Google 搜索采用的方法不一定适合所有人,而是经过仔细 A/B 测试,对各种投放选项组合进行 A/B 测试,直到找到最适合自己的解决方案。如果开发者的后端基础架构允许他们更快地部署更新,则可能希望浏览器始终忽略 HTTP 缓存,从而尽可能频繁地检查更新后的 Service Worker 脚本。如果您正在构建的单页应用可能会长时间保持打开状态,则可能不适合使用 skipWaiting() - 如果您允许新 Service Worker 在存在长期有效的客户端时激活,则可能会面临缓存不一致的风险

重点小结

默认情况下,Service Worker 对性能的影响

将 Service Worker 添加到 Web 应用意味着插入一段额外的 JavaScript,该一段 JavaScript 需要在 Web 应用收到其请求响应之前加载和执行。如果这些响应最终来自本地缓存而非网络,那么与采用缓存优先方案相比,运行 Service Worker 的开销通常可以忽略不计。但是,如果您知道您的 Service Worker 在处理导航请求时始终必须咨询网络,那么使用导航预加载功能可以提高性能。

Service Worker(仍然)是渐进式增强

Service Worker 的支持案例比一年前更精彩。所有现代浏览器现在都至少支持 Service Worker,但遗憾的是,有些高级 Service Worker 功能(如后台同步和导航预加载)并未全面推出。对确定您需要的特定功能子集进行特征检查,并且仅在存在 Service Worker 时注册它们,仍然是一个合理的方法。

同样,如果您在野外运行实验,并知道低端设备最终会导致性能不佳,从而导致 Service Worker 产生额外的开销,您同样可以避免在此类情况下注册 Service Worker。

您应该继续将 Service Worker 视为一种渐进式增强功能,在满足所有前提条件且 Service Worker 能够提升用户体验和提升整体加载性能时,会将其添加到 Web 应用中。

衡量一切指标

要确定运送 Service Worker 是对用户体验产生积极影响还是消极影响,唯一的方法是进行实验并衡量结果。

设置有意义测量的具体细节取决于您使用的分析服务提供商以及您通常在部署设置中进行实验的方式。此案例研究根据在 Google I/O Web 应用中使用 Service Worker 的体验,详细介绍了使用 Google Analytics(分析)收集指标的方法。

非目标

虽然 Web 开发社区中的许多人都将 Service Worker 与渐进式 Web 应用相关联,但构建“Google 搜索 PWA”并不是该团队的初始目标。Google 搜索 Web 应用目前不通过 Web 应用清单提供元数据,也不鼓励用户完成“添加到主屏幕”流程。搜索团队目前对通过 Google 搜索的传统入口点访问其 Web 应用的用户感到满意。

我们最初的重点在于逐步增强现有网站,而不是试图将 Google 搜索的网页体验提升到与您期望的安装版应用同等的效果。

致谢

感谢整个 Google 搜索 Web 开发团队在 Service Worker 实现方面所付出的努力,以及分享撰写本文的背景资料。要特别感谢 Philippe Golle、Rajesh Jagaannathan 和 R. Samuel Klatchko、Andy Martone、Leonardo Peña、Rachel Shearer、Greg Terrono 和 Clay Woolam。

更新(2021 年 10 月):自本文最初发布以来,Google 搜索团队重新评估了当前 Service Worker 架构的优势和权衡。上述 Service Worker 将停用。随着 Google 搜索 Web 基础架构的不断发展,该团队可能会重新审视其 Service Worker 设计。