这就是我所谓的服务人员!

服务人员API是Dremel网络平台。它提供了令人难以置信的广泛实用,同时也产生了弹性和更好的性能。如果你还没有用过Service Worker——如果是的话,你也不应该被指责到2020年,它还没有被广泛采用- 它是这样的东西:

继续下面的条
  1. 在初步访问网站时,浏览器寄存器数额是多少,搭载一台客户端代理相当少量的JavaScript像Web worker一样在自己的线程上运行。
  2. 在服务工作者的注册之后,您可以拦截请求并决定如何响应它们服务职工的拿来()事件

你决定如何处理你拦截的请求:a)你的电话,b)取决于你的网站。你可以重写请求precache静态资产在安装期间,提供离线功能这也是我们最终的关注点提供更小的HTML有效负载和更好的性能重复访客。

获得走出困境# section2

“每周木材”是我的一个客户,在威斯康辛州中部提供伐木服务。对他们来说,快速的网站是至关重要的。他们的业务位于Waushara县就像美国的许多乡村一样,网络质量和可靠性都不是很好

威斯康星州Waushara County的无线覆盖地图的屏幕截图,具有彩色覆盖层。大多数覆盖层都是彩色棕褐色的棕褐色,它代表了该县的区域,其在每秒3到9.99兆比特之间的下行链路速度。有稀疏的蓝色和深蓝色区域,表明服务更快,但远非是县域的大多数。
图1所示。威斯康星州沃沙拉县的无线覆盖地图。地图的棕色区域表示下行速度在3到9.99 Mbps之间。红色区域的速度更慢,而淡蓝色和深蓝色区域的速度更快。

威斯康星州有农田,但它也有大量的森林。当你需要一家伐木公司时,谷歌可能是你的第一站。如果你在一个糟糕的网络连接上等待太久,那么一家伐木公司的网站有多快就足以让你去别处寻找。

一开始,我并不认为每周木材网站需要服务工人。毕竟,如果一开始就足够快,为什么要把事情复杂化呢?另一方面,我知道我的客户不仅服务于沃沙拉县,而且服务于威斯康辛州中部的大部分地区,即使是一个基本的服务工作者也可以逐步增强那些最需要的地方的恢复能力。

我为客户网站编写的第一个Service Worker——从今以后我将其称为“标准”Service Worker——使用了三个证据充分的缓存策略

  1. 所有页面预缓存CSS和JavaScript的资产时,服务人员安装了窗口的load事件触发时。
  2. 服务静态资产CacheStorage如果可供使用的话。如果静态资产不在CacheStorage,从网络中检索,然后将其缓存以备以后访问。
  3. 对于HTML资产,首先击中网络并将HTML响应放入CacheStorage.如果下一次访问者到达时网络不可用,则从该站点提供缓存的标记CacheStorage

这些都不是新的或特殊的策略,但它们提供了两个好处:

  • 离线功能,当网络条件是斑点时,这很方便。
  • 加载静态资产的性能提升。

这种性能提升转化为42%和48%的中位数时间首先Contentful涂料(FCP)最大含量涂料(LCP),分别。更好的是,这些见解是基于真实用户监控(RUM).这意味着这些收获不仅仅是理论上的,而是对现实生活中的人们的真正改善。

Chrome开发工具中的请求/响应时间的截图。它描述了一个service worker在页面上为CacheStorage的静态资产服务,时间大约为23毫秒。
图2。在Chrome的开发人员工具中描述的请求/响应时间的细分。该请求是用于静态资产CacheStorage.因为Service Worker不需要访问网络,所以“下载”资产大约需要23毫秒CacheStorage

这种性能提升来自于完全绕过网络,使用已经存在的静态资产CacheStorage——尤其是render-blocking样式表。当我们依赖HTTP缓存时,也可以实现类似的好处,只有我刚刚描述的FCP和LCP改进与没有安装Service Worker的启动HTTP缓存页面相比。

如果你想知道为什么CacheStorageHTTP缓存是不相等的,这是因为HTTP缓存——至少在某些情况下-可能仍然需要访问服务器来验证资产的新鲜度。cache - control是不可变的Flag绕过了这个,但不可变的没有很好的支持然而。一个很长的max-age值也可以,但是Service Worker API和CacheStorage为您提供了更多的灵活性。

除了详细信息,外带是最简单,最良好的服务工作者缓存实践可以提高性能。潜在的不仅仅是配置得很好缓存控制头可以提供。即便如此,Service Worker仍然是一项具有更多可能性的令人难以置信的技术。走得更远是有可能的,我会告诉你怎么做。

一个更好、更快的Service Worker# section3

网络它本身就是一些“创新”,这是一个我们同样喜欢到处炫耀的词。对我来说,真正的创新不是我们仅仅为了开发人员的利益而创建新的框架或模式,而是这些发明是否有利于那些最终使用我们在网络上提供的任何东西的人。选民的优先次序是我们应该尊重的。用户永远高于一切。

Service Worker API的创新空间相当大。你在这个空间里的工作方式会对你的网络体验产生很大的影响。之类的东西导航预加载ReadableStream让Service Worker从优秀变成了杀手。我们可以用这些新功能分别做以下事情:

  • 通过并行化服务工作者启动时间来减少服务工作者延迟导航请求
  • 流内容从CacheStorage和网络。

此外,我们将结合这些功能,并拿出一个更多的技巧:预切页眉和页脚的偏导数,然后将它们与来自网络的内容偏导数相结合。这不仅减少了我们从网络上下载的数据,而且还提高了重复访问的感知性能。这是帮助每个人的创新。

头发花白,我转身对你说“我们这样做。”

铺设基础#第四单元

如果动态地将缓存的页眉和页脚部分与网络内容结合起来的想法看起来像单页应用程序(Single Page Application, SPA),那么您离这个想法不远了。与SPA一样,您需要将“应用程序外壳”模型应用到您的网站。只是,你必须把你的网站看作三个独立的部分,而不是一个客户端路由器将内容分解成一个最小的标记:

  • 头。
  • 内容。
  • 页脚。

我客户的网站是这样的:

“每周木材”网站的屏幕截图,用颜色标注了组成页面的每个部分。标题用蓝色编码,页脚用红色编码,中间的主要内容用黄色编码。
图3。每周木材网站的不同部分的颜色编码。页脚和标题部分存储在CacheStorage,虽然用户离线,否则从网络中检索内容部分。

这里要记住的是,个人部分不必有效标记,因为它的所有标签都需要在每个部分内关闭。最终意义上唯一重要的是这些部分的组合必须是有效的标记。

首先,你需要安装服务人员时,预先缓存分开的页眉和页脚谐音。对于我的客户的网站,这些谐音从供应/部分标头/ partial-footer路径名:

self.addEventListener( “安装”,事件=> {常量cacheName = “fancy_cache_name_here”;常量precachedAssets = [ “/部分报头”,//头部部分 “/部分英尺”,//页脚局部//其他资产价值预先缓存]; event.waitUntil(caches.open(cacheName)。然后(缓存=> {返回cache.addAll(precachedAssets);}。)然后,(()=> {返回self.skipWaiting();}));});

每个页面都必须是可获取的内容部分减去页眉和页脚,以及整个页面的页眉和页脚。这很关键,因为对页面的首次访问不会由Service Worker控制。一旦Service Worker接管,您就可以提供内容部分,并使用来自的页眉和页脚部分将它们组装成完整的响应CacheStorage

如果您的站点是静态的,这意味着生成一大堆标记部分,您可以在Service Worker中重写请求拿来()事件。如果你的网站有一个后端,就像我的客户端一样,你可以使用HTTP请求头来指示服务器发送完整的页面或内容部分。

困难的部分是把所有的拼在一起,但我们会做到这一点。

把它们放在一起# section5

即使编写一个基本的Service Worker也很有挑战性,但是当将多个响应组装成一个响应时,事情会变得非常复杂。这样做的一个原因是,为了避免Service Worker的启动代价,我们需要设置导航预加载。

实现导航预加载# section6

导航预加载解决了Service Worker启动时间的问题,该问题会延迟对网络的导航请求。你最不想和Service Worker一起做的事就是妨碍演出。

导航预加载必须显式启用。一旦启用,Service Worker将不会在启动期间等待导航请求。在Service Worker中启用导航预加载激活事件:

自我。一个ddEventListener("activate", event => { const cacheName = "fancy_cache_name_here"; const preloadAvailable = "navigationPreload" in self.registration; event.waitUntil(caches.keys().then(keys => { return Promise.all([ keys.filter(key => { return key !== cacheName; }).map(key => { return caches.delete(key); }), self.clients.claim(), preloadAvailable ? self.registration.navigationPreload.enable() : true ]); })); });

因为导航预加载不是所有地方都支持的,我们必须进行通常的功能检查,我们在上面的示例中存储preloadAvailable变量。

另外,我们需要使用Promise.all ()在Service Worker激活之前解析多个异步操作。这包括修剪那些旧的缓存,以及等待两者clients.claim ()(这告诉Service Worker立即断言控制,而不是等到下一次导航)并启用导航预加载。

三元操作符用于在支持的浏览器中启用导航预加载,并避免在不支持的浏览器中抛出错误。如果preloadAvailable真正的,我们使导航预紧力。如果不是,我们通过一个布尔值,不会影响如何Promise.all ()解决了。

启用导航预加载后,我们需要在Service Worker中编写代码拿来()事件处理程序来使用预加载的响应:

self.adeventListener(“fetch”,event => {const {请求} =事件; //为简洁起见省略省略的静态资产处理代码// ... //检查是否这是对文档(Request.mode =的请求)== "navigate") { const networkContent = Promise.resolve(event.preloadResponse).then(response => { if (response) { addResponseToCache(request, response.clone()); return response; } return fetch(request.url, { headers: { "X-Content-Mode": "partial" } }).then(response => { addResponseToCache(request, response.clone()); return response; }); }).catch(() => { return caches.match(request.url); }); // More to come... } });

虽然这不是在服务人员的全部拿来()事件代码,有很多需要解释:

  1. 预加载的响应可用event.preloadResponse.然而,正如杰克·阿奇博尔德所说的价值event.preloadResponse未定义的在不支持导航预加载的浏览器中。因此,我们必须通过event.preloadResponsePromise.resolve ()避免兼容性问题。
  2. 我们适应结果然后打回来。如果事件。preloadresponse.时,我们使用预加载响应并将其添加到CacheStorage通过一个addResponseToCache ()helper函数。如果没有,我们发送拿来()使用自定义向网络请求以获取内容部分X-Content-Mode值为的头文件部分的
  3. 如果网络不可用,我们将返回最近访问的内容部分CacheStorage
  4. 响应 - 无论它从中获取它 - 然后返回到命名的变量networkContent我们以后会用到。

如何检索内容部分是很棘手的。启用导航预加载后,一个特殊的Service-Worker-Navigation-Preload值为的头文件真正的被添加到导航请求中。然后,我们在后端使用该标题,以确保响应是内容部分而不是完整页标记。

然而,由于导航预加载在所有浏览器中都不可用,我们在这些场景中发送了不同的头部。在《每周木材》的案例中,我们又回到了惯例X-Content-Mode头。在我的客户端PHP后端,我创建了一些方便的常量:

<?php //这是一个导航预加载请求?define("NAVIGATION_PRELOAD", isset($_SERVER["HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD"]) && strstr ($_SERVER["HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD"], "true") !== false);//这是一个内容部分的显式请求?定义("PARTIAL_MODE", isset($_SERVER["HTTP_X_CONTENT_MODE"]) && stristr($_SERVER["HTTP_X_CONTENT_MODE"], "partial") !== false);//如果其中一个为真,这是一个内容部分定义的请求("USE_PARTIAL", NAVIGATION_PRELOAD === true || PARTIAL_MODE === true);? >

从那里,USE_PARTIALConstant用于调整响应:

<?php if (USE_PARTIAL === false) {require_once("partial-header.php");} require_once(“包括/ home”);if (USE_PARTIAL === false) {require_once("part -footer.php");} ? >

这里需要注意的是,你应该指定一个不同为HTML响应采取Service-Worker-Navigation-Preload(在本例中,是X-Content-Modeheader)考虑到HTTP缓存的目的——假设您正在缓存HTML,但这可能不是您的情况。

完成导航预加载后,我们就可以开始从网络流内容部分,并将它们与来自页眉和页脚部分拼接在一起CacheStorage转换为Service Worker将提供的单个响应。

流媒体部分内容并拼接在一起反应# section7

而页眉和页脚的部分将几乎是即时可用的,因为它们已经在CacheStorage由于服务人员的安装,它的内容的部分,我们从网络将成为瓶颈检索。这是因此,我们至关重要流的反应所以我们可以尽快开始将标记推到浏览器。ReadableStream可以为我们做这件事。

ReadableStream企业是一记弯管机。任何人谁告诉你这是“容易”被耳边甜言蜜语给你。它的难的.之后,我写我自己的功能合并流反应和搞砸了关键的一步,它最终没有提高页面的性能,你要知道,我修改杰克阿奇博尔德mergeResponses ()函数为了满足我的需要:

async函数mergerresponses (responsePromises) {const readers = responsePromises。map(responsePromise => {return Promise.resolve(responsePromise)。然后(response => {return response.body. getreader ();});});让doneResolve doneReject;const done = new Promise((resolve, reject) => {donerresolve = resolve;doneReject =拒绝;});const readable = new ReadableStream({async pull (controller) {const reader = await readers[0];Try {const {done, value} = await reader.read();If (done) {reader .shift(); if (!readers[0]) { controller.close(); doneResolve(); return; } return this.pull(controller); } controller.enqueue(value); } catch (err) { doneReject(err); throw err; } }, cancel () { doneResolve(); } }); const headers = new Headers(); headers.append("Content-Type", "text/html"); return { done, response: new Response(readable, { headers }) }; }

像往常一样,有很多事情在发生:

  1. mergeResponses ()接受一个名为responsePromises,这是阵列响应从一个导航预加载返回的对象,拿来(),或caches.match ().假设网络可用,这将始终包含三个响应:两个来自caches.match ()(希望)还有一个来自网络。
  2. 然后我们才能在responsePromises数组,必须映射responsePromises到包含每个响应一个读出器的阵列。每个读者在后面的使用ReadableStream ()构造函数流的每个响应的内容。
  3. 命名一诺完毕被创建。在它里面,我们分配承诺解决()拒绝()外部变量的函数doneResolvedoneReject,分别。这些将用于ReadableStream ()指示流是否已完成或遇到了障碍。
  4. ReadableStream ()实例的名称为可读取的.当响应从CacheStorage和网络,他们的内容将被追加可读取的
  5. 流的拉()方法在数组中流化第一个响应的内容。如果流没有以某种方式被取消,则通过调用读取器数组的方式丢弃每个响应的读取器转移()方法当响应完全流动时。这重复,直到没有更多的读者来处理。
  6. 当所有操作完成后,合并的响应流将作为单个响应返回,并使用内容类型的报头值text / html

这是更简单如果你使用TransformStream,但这可能不是每个浏览器都可以选择的选项,这取决于您何时阅读本文。现在,我们必须坚持这种方法。

现在让我们再来看看Service Worker拿来()事件,并应用mergeResponses ()功能:

self.adeventListener(“fetch”,event => {const {请求} =事件; //为简洁起见省略省略的静态资产处理代码// ... //检查是否这是对文档(Request.mode =的请求)==“导航”){//导航预加载/ fetch()省略回退码。// ... const {dode,response} =等待兼并【caches.match(“/ partial-header”),networkContent,缓存.match(“/ partial-footer”)]); event.waituntil(dode); event.respondwith(响应);}});

最后拿来()事件处理程序,我们传递页眉和页脚的部分CacheStorage到了mergeResponses ()功能,并将结果传递给拿来()事件的以(应对)方法,该代表服务工作人员提供合并的响应。

结果值得这么麻烦吗?# section8

要做的事情很多,而且很复杂!你可能会搞砸一些东西,或者你的网站架构并不适合这种方法。因此,重要的是要问:性能收益值得这么做吗?在我看来呢?是的!合成性能的提高一点也不差:

一条条形图比较了第一个满足的涂料和每周木材网站的最大的满足油漆性能,以便没有服务工作者,一个“标准”服务工作者和缝合来自CoChustorage和网络的内容部分的流媒体服务工作者。前两种情况基本相同,而流媒体服务工作者为FCP和LCP提供了可观的性能 - 特别是对于FCP!
图4。这是一个柱状图,显示了每周木材网站上不同类型的Service Worker的FCP和LCP合成性能数据的中值。

综合测试不测量任何东西的性能,除了特定的设备和它们执行的互联网连接。即便如此,这些测试都是在我客户网站的分期版本上进行的诺基亚2安卓手机在Chrome的开发工具上限制了“快速3G”连接。每个类别都在主页上测试了10次。我们的结论如下:

  • 没有任何Service Worker比“标准”Service Worker稍微快一点,但缓存模式比流变体更简单。就像,稍微快一点。这可能是由于Service Worker启动带来的延迟,但是,我将介绍的RUM数据显示了不同的情况。
  • 在没有Service Worker或使用“标准”Service Worker的场景中,LCP和FCP都是紧密耦合的。这是因为页面的内容非常简单,CSS也非常小。内容最丰富的段落通常是一页的开头段落。
  • 然而,流服务Worker解耦了FCP和LCP,因为头内容部分流直接从CacheStorage
  • 流式Service Worker中的FCP和LCP都低于其他情况。
条形图比较没有服务工作者,“标准”服务的工作人员,一个流媒体服务工作者的RUM中位数FCP和LCP的性能。无论是“标准”和流媒体服务人员提供更好的FCP和LCP性能上没有服务人员,但在FCP性能的流媒体服务人员过人之处,而只有在LCP比“标准”的服务人员稍慢之中。
图5。这是一个柱状图,显示了每周木材网站不同类型的Service Worker中FCP和LCP朗姆酒性能数据的中位数。

流媒体Service Worker对真实用户的好处是显而易见的。对于FCP,我们比没有Service Worker有79%的改进,比“标准”Service Worker有63%的改进。LCP的好处更加微妙。与没有Service Worker相比,我们意识到lcp有41%的改进——这是不可思议的!然而,与“标准”Service Worker相比,LCP稍微慢一些。

因为性能的长尾效应是重要的,让我们看看第95百分位的FCP和LCP表现:

条形图比较没有服务工作者,“标准”服务的工作人员,一个流媒体服务工作者的RUM中位数FCP和LCP的性能。无论是“标准”和流媒体服务人员在所有比没有服务人员更快,但流媒体服务人员击败了“标准”的服务人员两种FCP和LCP。
图6。在每周木材网站的各种服务工作人员类型中为95百分位FCP和LCP RUM性能数据的条形图。

百分RUM数据的95个是评估最慢的经验的好地方。在这种情况下,我们看到,流媒体服务工作者都分别赋予40%和51%的改善FCP和LCP,在没有服务人员。相比,“标准”服务工人,我们看到由分别为19%和43%,在FCP的降低和LCP。如果这些结果似乎有点松鼠相比,合成指标,请记住:这是RUM数据为您服务!你永远不知道谁去访问你的网站所用的设备是什么网络上。

虽然FCP和LCP都被流式传输,导航预加载(在Chrome的情况下)升高,但通过从两者缝合局部拼接来发送更少的标记CacheStorage就网络而言,FCP显然是赢家。从知觉上讲,好处是显而易见的,正如这段视频所暗示的:

图7。每周木材主页的重复景观的3个WebPageTest视频。左边是页面不是由服务人员控制,涂底漆的HTTP缓存。右边是在同一页通过流媒体服务工作者控制,CacheStorage催芽。

现在问自己:如果这是我们可以期望在这样一个小型和简单的网站上的改进,我们可能在一个具有较大页眉和页脚标记有效载荷的网站上的预期?

警告和结论# section9

在开发方面有交易吗?是的。

正如菲利普·沃尔顿指出,缓存的头部分意味着文档标题必须在每次导航时通过更改的值在JavaScript中更新document.title.这也意味着你需要在JavaScript中更新导航状态,以反映当前页面,如果这是你在网站上做的事情。请注意,这应该不会导致索引问题,因为Googlebot抓取的页面没有启动缓存。

在使用身份验证的站点上也可能存在一些挑战。例如,如果您的站点标题在登录时显示当前经过身份验证的用户,您可能必须更新由CacheStorage以反映验证对象。您可以通过将基本用户数据存储在localStorage并从那里更新UI。

当然还有其他挑战,但它可以衡量你面对用户的益处与开发成本。在我看来,这种方法在博客,营销网站,新闻网站,电子商务和其他典型用例等应用中具有广泛的适用性。

不过,总的来说,它与您从SPA获得的性能改进和效率增益类似。唯一不同的是,你并没有替换经过时间考验的导航机制,也没有解决所有需要解决的问题,但是加强他们。我认为,在客户端路由风行一时的情况下,这一点非常重要。

工具箱你可能会问——你这样做是对的。在使用Service Worker API时,Workbox简化了很多,使用它也没有错。就我个人而言,我更喜欢尽可能接近金属,这样我就可以更好地理解像Workbox这样的抽象背后的东西。即便如此,“服务工作者”还是很难。如果适合您,请使用Workbox。就框架而言,它的抽象成本非常低。

不管这种做法,我觉得有一个在使用服务工作者API,以减少标记的发送量惊人的效用和力量。它有利于我的客户和所有使用他们的网站的人。由于服务人员,并围绕其使用的创新,我的客户的网站是在威斯康星州的偏远地区的更快。这件事情我觉得不错。

特别感谢杰克阿奇博尔德感谢他宝贵的编辑建议,说得婉转些,这些建议大大提高了这篇文章的质量。

暂无评论

有话要说吗?

我们已经关闭了评论功能,但是你可以看到在我们关闭评论功能之前人们都说了些什么。

从阿拉巴马州

保持你的设计思路新鲜

在这段出自《RECOGNIZE》第二卷的摘录中,Regine Gilbert提供了一个有用的助记方法,帮助我们从一个全新的角度来看待我们的设计工作:“WOQE”,用于观察、观察、提问和探索。
创造力