A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

hace 5 años
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. title: ServiceWorker: Revolution of the Web Platform
  2. url: https://ponyfoo.com/articles/serviceworker-revolution
  3. hash_url: 2126318176fe110654aa27abf77b6603
  4. <section class="at-teaser"><div class="at-teaser-markdown md-markdown"><p>While not the most amusingly named feature of the web platform, everything seems to point at ServiceWorker being the <strong>most significant addition</strong> to the web platform since the introduction of <em>AJAX</em> &#x2013; over 10 years ago. Not to be confused with WebWorker <em>(used to offload intense compute operations onto another execution thread)</em>, ServiceWorker allows you to intercept <em>(and hijack)</em> network requests originating from your site before they&#x2019;re even dispatched. This article explores how it works, what it means and what it enables, and how you can implement it by following a case study.</p></div></section><section class="at-container de-host"><div class="de-column"><article itemscope="itemscope" itemtype="http://schema.org/BlogPosting" class="at-article ly-section"><meta itemprop="dateCreated" content="2015-10-21T03:22:23+00:00"/><meta itemprop="dateModified" content="2015-10-28T07:10:17+00:00"/><meta itemprop="datePublished" content="2015-10-23T11:54:37.484Z"/><meta itemprop="keywords" content="serviceworker,offline-first"/><meta itemprop="discussionUrl" content="https://ponyfoo.com/articles/serviceworker-revolution#comments"/><meta itemprop="interactionCount" content="9 UserComments"/><section itemprop="articleBody" class="at-corpus"><section itemprop="about" class="md-markdown at-introduction"><p>Sitting between humans and websites, ServiceWorker puts you in the driver&#x2019;s seat. When a ServiceWorker is installed, you&#x2019;ll be able to deliver responses to requests from the service worker <em>(which lies on the client-side)</em>, without necessarily hitting the network.</p><p>ServiceWorker is the evolution of the AppCache manifest. AppCache has been bashed for years because of several issues &#x2013; that were <a href="http://alistapart.com/article/application-cache-is-a-douchebag" aria-label="Application Cache is a Douchebag - alistapart.com">extensively documented</a> &#x2013; in a resonating <em>&#x201C;AppCache sucks&#x201D;</em> roar across the blogosphere. Most of the issues in AppCache revolved around it being a simple-looking declarative interface to offline behavior that, behind the curtains, had <strong>a complicated ruleset that wasn&#x2019;t simple nor intuitive.</strong></p><blockquote><p>The ApplicationCache spec is like an onion: it has many many layers, and as you peel through them you&#x2019;ll be reduced to tears.<br><em>&#x2013; <a href="https://twitter.com/jaffathecake" aria-label="@jaffathecake on Twitter">Jake Archibald</a></em></p></blockquote><p>On the other hand, ServiceWorker offers a programmatic API that allows you to accomplish so much more than AppCache ever could. Instead of almost-too-magical rules around a simplistic declarative syntax, ServiceWorker relies on plain JavaScript code.</p><p>Even though it doesn&#x2019;t have perfect <a href="https://jakearchibald.github.io/isserviceworkerready/" aria-label="Is ServiceWorker Ready?">browser support today</a>, ServiceWorker is just the beginning. Once you implement ServiceWorker in your sites, you&#x2019;ll be able to provide offline functionality for your pages. Yes, even when the human doesn&#x2019;t have any network connectivity at all, <strong>they&#x2019;ll be able to access your site while offline</strong>, provided that they&#x2019;ve visited at least once before. And that&#x2019;s <em>just the basic offering</em>. ServiceWorker also enables you to send <strong>Push Notifications</strong>, just like mobile apps do, and even when the website is running in the background; you also get to use <strong>Background Sync</strong>, allowing you to execute one-time or periodic updates of the content in your site while it&#x2019;s backgrounded; and <strong>Add to Home Screen</strong>, which does exactly what it sounds like, making your website feel almost like a native app.</p></section><section class="md-markdown at-body"><p><img alt="A picture of clocks" data-src="https://i.imgur.com/SMFZGJ6.jpg" class="js-only"><noscript><img src="https://i.imgur.com/SMFZGJ6.jpg" alt="A picture of clocks"></noscript></p><p>I&#x2019;ve spent some time playing around with the ServiceWorker API and felt compelled to write about it. While brand new, this API is a web platform powerhouse that we must become acquainted with if we want to have a fighting chance when it comes to mobile devices. Furthermore, ServiceWorker is great for performance besides its offline capabilities, as it grants fine-grained control over caching. Remember those pesky <em>&#x201C;this gravatar isn&#x2019;t cached for that long&#x201D;</em> messages on PageSpeed? You can use ServiceWorker as an intermediary cache that caches them for far longer!</p><p>If you have a personal website, blog, or small site you use to toy around with, I encourage you to make your way through this guide by trying to implement ServiceWorker on your own site. I did that with <a href="/" aria-label="You&apos;re looking at it, dummy!"><code class="md-code md-code-inline">ponyfoo.com</code></a> and besides being a great thought exercise it feels great to know that now we can access these articles while offline, too.</p><h1 id="getting-started">Getting Started</h1><p>Before getting started, we should go over a few notes on ServiceWorker.</p><ul><li>ServiceWorker doesn&#x2019;t have DOM access &#x2013; it&#x2019;s a worker that runs outside of the scope of any one page</li><li>ServiceWorker relies heavily on promises, you&#x2019;ll have to feel comfortable with them <a href="/articles/es6-promises-in-depth" aria-label="ES6 Promises in Depth on Pony Foo"><em>(read the guide)</em></a></li><li><a href="http://www.html5rocks.com/en/tutorials/service-worker/introduction/#toc-before" aria-label="Introduction to Service Worker on html5rocks.com"><code class="md-code md-code-inline">https</code> is required</a> for deployed sites to leverage ServiceWorker, although you can test on <code class="md-code md-code-inline">http://localhost</code> just fine</li></ul><p>ServiceWorker is a powerful addition to the web platform, having the ability to intercept all requests originating from a site &#x2013; including those made against other origins <em>(e.g: <code class="md-code md-code-inline">i.imgur.com</code>)</em>. For that reason, the <code class="md-code md-code-inline">https</code> requirement is a given for security concerns.</p><p>The quintessential ServiceWorker installation example is in the code snippet below. This is what I&#x2019;ve used in <a href="https://github.com/ponyfoo/ponyfoo/blob/e5052ab1545d929f192f6fd7fdbf3e44e10c8eae/client/js/main.js#L40-L42" aria-label="ServiceWorker registration for ponyfoo on GitHub"><code class="md-code md-code-inline">ponyfoo/ponyfoo</code></a> as well, and it shows how ServiceWorker is easily a progressive enhancement, as this is all the code in the main client-side codebase that references it. The feature test ensures we don&#x2019;t break older browsers, and the rest of the ServiceWorker-enabled code will live in the <code class="md-code md-code-inline">service-worker.js</code> script.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-keyword">if</span> (<mark class="md-mark md-code-mark"><span class="md-code-string">&apos;serviceWorker&apos;</span> <span class="md-code-keyword">in</span> navigator</mark>) {
  5. navigator.serviceWorker.register(<span class="md-code-string">&apos;/service-worker.js&apos;</span>);
  6. }
  7. </code></pre><p>It should be noted that a ServiceWorker is scoped according to the endpoint we use to register the worker. If we just use <code class="md-code md-code-inline">/service-worker.js</code> then the scope is the entire origin, but if you register a ServiceWorker with an endpoint like <code class="md-code md-code-inline">/js/service-worker.js</code>, it&#x2019;ll only be able to intercept requests on your origin scoped to <code class="md-code md-code-inline">/js/</code>, such as <code class="md-code md-code-inline">/js/all.js</code> &#x2013; not at all useful.</p><p>Once a ServiceWorker is registered, it&#x2019;ll be downloaded and executed. After that, the first step in its lifecycle will be to fire the <code class="md-code md-code-inline">install</code> event. You can register event listeners that will fire at this time in your <code class="md-code md-code-inline">service-worker.js</code> file.</p><p>The example below is a simplified version of how I install the ServiceWorker for Pony Foo. The <code class="md-code md-code-inline">event.waitUntil</code> method takes a promise and then waits for it to be settled. If the promise fulfills, the ServiceWorker becomes installed, and otherwise the installation fails. ServiceWorker has access to the <code class="md-code md-code-inline">caches</code> API that can be used as an intermediate cache that lives on the client-side.<br>As you can observe in the code below, the <code class="md-code md-code-inline">caches</code> API is heavily <code class="md-code md-code-inline">Promise</code>-based as well. After opening the <code class="md-code md-code-inline">v1::fundamentals</code> cache we use <code class="md-code md-code-inline">cache.addAll</code> to store <code class="md-code md-code-inline">GET</code> responses for each of the <code class="md-code md-code-inline">offlineFundamentals</code> resources in said <code class="md-code md-code-inline">cache</code>.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-keyword">var</span> offlineFundamentals = [
  8. <span class="md-code-string">&apos;/&apos;</span>,
  9. <span class="md-code-string">&apos;/offline&apos;</span>,
  10. <span class="md-code-string">&apos;/css/all.css&apos;</span>,
  11. <span class="md-code-string">&apos;/js/all.js&apos;</span>
  12. ];
  13. self.addEventListener(<mark class="md-mark md-code-mark"><span class="md-code-string">&apos;install&apos;</span></mark>, <span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">installer</span> <span class="md-code-params">(event)</span> </span>{
  14. <mark class="md-mark md-code-mark">event.waitUntil</mark>(
  15. caches
  16. .open(<span class="md-code-string">&apos;v1::fundamentals&apos;</span>)
  17. .then(<span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">prefill</span> <span class="md-code-params">(cache)</span> </span>{
  18. <mark class="md-mark md-code-mark">cache.addAll</mark>(offlineFundamentals);
  19. })
  20. );
  21. });
  22. </code></pre><p>Why am I caching those particular resources? Because visitors will be able to visit the home page of my site while offline. Additionally, the <code class="md-code md-code-inline">/offline</code> resource can be used as a default offline response for <code class="md-code md-code-inline">Content-Type: application/html</code> requests made against endpoints in my origin <em>(e.g: <a href="/articles/history" aria-label="Full Publication History on Pony Foo"><code class="md-code md-code-inline">ponyfoo.com/articles/history</code></a> while offline)</em>, as we&#x2019;ll see later on.</p><blockquote><p>You can inspect the ServiceWorker lifecycle on DevTools. It&#x2019;ll make your life much easier while debugging! Just go to the Resources tab and choose the last option in there &#x2013; Service Workers. Available starting in Chrome 48 <em>(already in Chrome Canary)</em>.</p><p><img alt="ServiceWorker lifecycle on DevTools" data-src="https://i.imgur.com/VqGQPFY.png" class="js-only"><noscript><img src="https://i.imgur.com/VqGQPFY.png" alt="ServiceWorker lifecycle on DevTools"></noscript></p></blockquote><p>Once those resources are successfully cached, the ServiceWorker becomes <code class="md-code md-code-inline">installed</code>.</p><h1 id="serviceworker-lifecycle">ServiceWorker Lifecycle</h1><p>In addition to <code class="md-code md-code-inline">installed</code> &#x2013; and <code class="md-code md-code-inline">installing</code> while waiting on the cache to be filled up &#x2013; there&#x2019;s also an activation step during which an older ServiceWorker becomes <code class="md-code md-code-inline">redundant</code> while the newer one replaces it and becomes <code class="md-code md-code-inline">activated</code> <em>(until then, the newer ServiceWorker is <code class="md-code md-code-inline">activating</code>)</em>. There&#x2019;s <a href="http://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-state-enum" aria-label="ServiceWorkerState enum in the ServiceWorker Specification">five possible states</a> in the ServiceWorker lifecycle.</p><ul><li><code class="md-code md-code-inline">installing</code> while blocked on <code class="md-code md-code-inline">event.waitUntil</code> promises during the <code class="md-code md-code-inline">install</code> event</li><li><code class="md-code md-code-inline">installed</code> while waiting to become active</li><li><code class="md-code md-code-inline">activating</code> while blocked on <code class="md-code md-code-inline">event.waitUntil</code> promises during the <code class="md-code md-code-inline">activate</code> event</li><li><code class="md-code md-code-inline">activated</code> when completely operational and able to intercept <code class="md-code md-code-inline">fetch</code> requests</li><li><code class="md-code md-code-inline">redundant</code> when being replaced by a newer ServiceWorker script version, or being discarded due to a failed <code class="md-code md-code-inline">install</code></li></ul><p>After a ServiceWorker is <code class="md-code md-code-inline">installed</code>, the <code class="md-code md-code-inline">activate</code> event fires, and the exact same drill is followed. During the activation step, you could clear up room in the <code class="md-code md-code-inline">caches</code>. Remember how I prefixed my cache as <code class="md-code md-code-inline">v1::</code> earlier? If I updated the fundamental files in my cache I could just <code class="md-code md-code-inline">.delete</code> the older cache and bump the version number, as seen below.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-keyword">var</span> version = <span class="md-code-string">&apos;v2::&apos;</span>;
  23. self.addEventListener(<mark class="md-mark md-code-mark"><span class="md-code-string">&apos;activate&apos;</span></mark>, <span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">activator</span> <span class="md-code-params">(event)</span> </span>{
  24. event.waitUntil(
  25. caches<mark class="md-mark md-code-mark">.keys()</mark>.then(<span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-params">(keys)</span> </span>{
  26. <span class="md-code-keyword">return</span> <mark class="md-mark md-code-mark">Promise.all</mark>(keys
  27. .filter(<span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-params">(key)</span> </span>{
  28. <span class="md-code-keyword">return</span> key.indexOf(version) !== <span class="md-code-number">0</span>;
  29. })
  30. .map(<span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-params">(key)</span> </span>{
  31. <span class="md-code-keyword">return</span> <mark class="md-mark md-code-mark">caches.delete(key)</mark>;
  32. })
  33. );
  34. })
  35. );
  36. });
  37. </code></pre><p>Now that you have an <code class="md-code md-code-inline">activated</code> ServiceWorker, you can begin intercepting requests.</p><h1 id="intercepting-requests-in-serviceworker">Intercepting Requests in ServiceWorker</h1><p>Whenever a network request would be initiated and a ServiceWorker is activated, a <code class="md-code md-code-inline">fetch</code> event is raised on that ServiceWorker instead. Event handlers for the <code class="md-code md-code-inline">fetch</code> event are expected to produce a response for the request, and they may or may not access the network.</p><p>In its simplest form, your ServiceWorker could just act as a network passthrough. In this case, the application would seldom be any different than without a ServiceWorker. Note that, by default, <code class="md-code md-code-inline">fetch</code> doesn&#x2019;t include credentials such as cookies and fails to make requests to third parties that don&#x2019;t support CORS.</p><pre class="md-code-block"><code class="md-code md-lang-javascript">self.addEventListener(<span class="md-code-string">&apos;fetch&apos;</span>, <span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">fetcher</span> <span class="md-code-params">(event)</span> </span>{
  38. event.respondWith(fetch(event.request));
  39. });
  40. </code></pre><p>Typically, you don&#x2019;t want to cache non-<code class="md-code md-code-inline">GET</code> responses, so those should probably be filtered out. The code below will default to a networked response for every <code class="md-code md-code-inline">POST</code>, <code class="md-code md-code-inline">PUT</code>, <code class="md-code md-code-inline">PATCH</code>, <code class="md-code md-code-inline">HEAD</code>, or <code class="md-code md-code-inline">OPTIONS</code> originating from the ServiceWorker&#x2019;s scope.</p><pre class="md-code-block"><code class="md-code md-lang-javascript">self.addEventListener(<span class="md-code-string">&apos;fetch&apos;</span>, <span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">fetcher</span> <span class="md-code-params">(event)</span> </span>{
  41. <span class="md-code-keyword">var</span> request = event.request;
  42. <span class="md-code-keyword">if</span> (request.method !== <span class="md-code-string">&apos;GET&apos;</span>) {
  43. event.respondWith(fetch(request)); <span class="md-code-keyword">return</span>;
  44. }
  45. <span class="md-code-comment">// handle other requests</span>
  46. });
  47. </code></pre><p>If you&#x2019;re looking for recipes to handle requests, the <a href="https://jakearchibald.com/2014/offline-cookbook/" aria-label="The offline cookbook - jakearchibald.com">offline cookbook</a> is a great place to look.</p><h1 id="strategies">Strategies</h1><p>There are several different strategies you can apply to resolving requests in your ServiceWorker. Here are the ones I&#x2019;ve found the most interesting.</p><h1 id="network-then-cached">Network then Cached</h1><blockquote><p>Fetch from the network first, and fall back to a cached response if <code class="md-code md-code-inline">fetch</code> fails.</p></blockquote><p>You don&#x2019;t get to leverage the caching capabilities of ServiceWorker when you&#x2019;re online with this strategy, because the network always comes first. For that reason, <code class="md-code md-code-inline">fetch</code>-first also has the drawback that an intermittent, unreliable, or very slow network connection will never produce responses, even though it may have a perfectly usable cache going to waste.</p><ul><li>Always hit the network for non-<code class="md-code md-code-inline">GET</code> requests</li><li>Hit the network<ul><li>If network request succeeds, use its <code class="md-code md-code-inline">response</code> and store it in the cache</li><li>If network request fails, try <code class="md-code md-code-inline">caches.match(request)</code><ul><li>If there&#x2019;s a cache hit, use that as the <code class="md-code md-code-inline">response</code></li><li>If there&#x2019;s no cache hit, attempt to fall back to <code class="md-code md-code-inline">/offline</code></li></ul></li></ul></li></ul><p>This flow is better at producing fresh content <em>(as opposed to stale cached responses)</em> than others, but isn&#x2019;t all that useful beyond entirely-offline <em>(as opposed to <strong>effectively-offline</strong> due to low connectivity)</em> improvements. Mobile devices won&#x2019;t take full advantage of this strategy because their connectivity might be very low but not low enough for the device to turn <code class="md-code md-code-inline">navigator.online</code> off, and so you end up in the same place as with no ServiceWorker.</p><h2 id="cached-then-network">Cached then Network</h2><blockquote><p>Look for a cached response first, but always fetch from the network regardless of cache state.</p></blockquote><p>This flow is similar to the previous one, except you go to the cache first. Here responses may be immediate and you see performance improvements across visits to any content that was previously cached.</p><ul><li>Always hit the network for non-<code class="md-code md-code-inline">GET</code> requests</li><li>Check <code class="md-code md-code-inline">caches.match(request)</code> to see if there&#x2019;s a cache hit</li><li>Hit the network, regardless of cache hits<ul><li>If network request succeeds, cache its response</li><li>If network request fails, attempt to fall back to <code class="md-code md-code-inline">/offline</code></li></ul></li><li>Return <code class="md-code md-code-inline">cached</code> response in case of cache hit, <code class="md-code md-code-inline">fetch</code> response otherwise</li></ul><p>Eventually, the cache becomes fresh again, because <code class="md-code md-code-inline">fetch</code> is always used &#x2013; regardless of cache hits.</p><p>The problem in that case is that the cache may be stale. Suppose you visit a page once. The worker uses <code class="md-code md-code-inline">fetch</code> and then the response is cached. When you visit the page a second time, you get the cached response from the last time immediately, and then a <code class="md-code md-code-inline">fetch</code> is executed that pulls the latest version into the cache. At that point you&#x2019;ve already applied the previous version, which isn&#x2019;t the latest content.</p><h2 id="cached-then-network-and-postmessage">Cached then Network and <code class="md-code md-code-inline">postMessage</code></h2><p>The previous flow could serve stale content, but you could update the UI when the updated response comes in. I haven&#x2019;t experimented with <a href="https://googlechrome.github.io/samples/service-worker/post-message/" aria-label="Service Worker postMessage() Sample"><code class="md-code md-code-inline">postMessage</code></a> at this point, so this is mostly a thought exercise. The <code class="md-code md-code-inline">postMessage</code> interface can be used to transmit messages between the worker and the browser tabs under its control.</p><p>Using the same flow as described in <a href="#cached-then-network">Cached then Network</a>, you could add messaging between the worker and the app so that when the cache is updated, any tabs that are on the same page as the updated cache endpoint get updated. Of course, the interaction should be carefully crafted. A combination of virtual DOM diffing and careful planning when dealing with cached resources other than HTML pages would go a long way towards making these updates seamless.</p><p>That being said, this approach is probably a bit too sophisticated for most applications. As usual, it all depends on your use case anyways.</p><h1 id="implementation">Implementation</h1><p>On this blog I went for the <a href="#cached-then-network">&#x201C;Cached then Network&#x201D;</a> approach. This is the code we had so far. It helped us bail on non-<code class="md-code md-code-inline">GET</code> requests.</p><pre class="md-code-block"><code class="md-code md-lang-javascript">self.addEventListener(<span class="md-code-string">&apos;fetch&apos;</span>, <span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">fetcher</span> <span class="md-code-params">(event)</span> </span>{
  48. <span class="md-code-keyword">var</span> request = event.request;
  49. <span class="md-code-keyword">if</span> (request.method !== <span class="md-code-string">&apos;GET&apos;</span>) {
  50. event.respondWith(fetch(request)); <span class="md-code-keyword">return</span>;
  51. }
  52. <span class="md-code-comment">// handle other requests</span>
  53. });
  54. </code></pre><p>After that, We can look for cache hits with <code class="md-code md-code-inline">caches.match(request)</code> and then respond with the result from a <code class="md-code md-code-inline">queriedCache</code> callback.</p><pre class="md-code-block"><code class="md-code md-lang-javascript">event<mark class="md-mark md-code-mark">.respondWith</mark>(caches
  55. <mark class="md-mark md-code-mark">.match</mark>(request)
  56. <mark class="md-mark md-code-mark">.then(queriedCache)</mark>
  57. );
  58. </code></pre><p>The <code class="md-code md-code-inline">queriedCache</code> method receives the <code class="md-code md-code-inline">cached</code> response, if any. It then makes a <code class="md-code md-code-inline">fetch</code> request regardless of the cache getting a hit. We also try to fall back gracefully when either fetch or caching fail, with an <code class="md-code md-code-inline">unableToResolve</code> callback. Lastly, we return the <code class="md-code md-code-inline">cached</code> response and fall back to the <code class="md-code md-code-inline">networked</code> promise if we miss the cache.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">queriedCache</span> <span class="md-code-params">(cached)</span> </span>{
  59. <span class="md-code-keyword">var</span> networked = <mark class="md-mark md-code-mark">fetch(request)</mark>
  60. .then(fetchedFromNetwork, unableToResolve)
  61. .catch(unableToResolve);
  62. <span class="md-code-keyword">return</span> cached || networked;
  63. }
  64. </code></pre><p>When <code class="md-code md-code-inline">fetch</code> succeeds and <code class="md-code md-code-inline">fetchedFromNetwork</code> is called, we store a copy of the response in the <code class="md-code md-code-inline">cache</code> and then we return the <code class="md-code md-code-inline">response</code> unchanged.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">fetchedFromNetwork</span> <span class="md-code-params">(response)</span> </span>{
  65. <span class="md-code-keyword">var</span> clonedResponse = response<mark class="md-mark md-code-mark">.clone()</mark>;
  66. caches.open(version + <span class="md-code-string">&apos;pages&apos;</span>).then(<span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">add</span> <span class="md-code-params">(cache)</span> </span>{
  67. <mark class="md-mark md-code-mark">cache.put(request, clonedResponse)</mark>;
  68. });
  69. <span class="md-code-keyword">return</span> response;
  70. }
  71. </code></pre><p>When we&#x2019;re unable to resolve <code class="md-code md-code-inline">fetch</code> requests we have to return a fallback. By default, we can make that an opaque <code class="md-code md-code-inline">offlineResponse</code>. As you can see, you can hardcode <code class="md-code md-code-inline">Response</code> objects and use those to react to network requests.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">unableToResolve</span> <span class="md-code-params">()</span> </span>{
  72. <span class="md-code-keyword">return</span> offlineResponse();
  73. }
  74. <span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">offlineResponse</span> <span class="md-code-params">()</span> </span>{
  75. <span class="md-code-keyword">return</span> <mark class="md-mark md-code-mark">new Response(<span class="md-code-string">&apos;&apos;</span>, { status: <span class="md-code-number">503</span>, statusText: <span class="md-code-string">&apos;Service Unavailable&apos;</span> })</mark>;
  76. }
  77. </code></pre><p>If we were dealing with an image, we could return some placeholder rainbows image instead &#x2013; provided that <code class="md-code md-code-inline">rainbows</code> is a URL string that was already cached during the ServiceWorker installation step.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">unableToResolve</span> <span class="md-code-params">()</span> </span>{
  78. <span class="md-code-keyword">var</span> accepts = request.headers.get(<span class="md-code-string">&apos;Accept&apos;</span>);
  79. <span class="md-code-keyword">if</span> (<mark class="md-mark md-code-mark">accepts.indexOf(<span class="md-code-string">&apos;image&apos;</span>) !== -<span class="md-code-number">1</span></mark>) {
  80. <span class="md-code-keyword">return</span> caches.match(rainbows);
  81. }
  82. <span class="md-code-keyword">return</span> offlineResponse();
  83. }
  84. </code></pre><p>Furthermore, if its a gravatar, we could use a tailored <code class="md-code md-code-inline">mysteryMan</code> image for that, also cached during installation.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">unableToResolve</span> <span class="md-code-params">()</span> </span>{
  85. <span class="md-code-keyword">var</span> url = <span class="md-code-keyword">new</span> URL(request.url);
  86. <span class="md-code-keyword">var</span> accepts = request.headers.get(<span class="md-code-string">&apos;Accept&apos;</span>);
  87. <span class="md-code-keyword">if</span> (accepts.indexOf(<span class="md-code-string">&apos;image&apos;</span>) !== -<span class="md-code-number">1</span>) {
  88. <span class="md-code-keyword">if</span> (<mark class="md-mark md-code-mark">url.host === <span class="md-code-string">&apos;www.gravatar.com&apos;</span></mark>) {
  89. <span class="md-code-keyword">return</span> caches.match(mysteryMan);
  90. }
  91. <span class="md-code-keyword">return</span> caches.match(rainbows);
  92. }
  93. <span class="md-code-keyword">return</span> offlineResponse();
  94. }
  95. </code></pre><p>Similarly, if it&#x2019;s a request that accepts HTML, we could return the <code class="md-code md-code-inline">/offline</code> view we had installed earlier.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">unableToResolve</span> <span class="md-code-params">()</span> </span>{
  96. <span class="md-code-keyword">var</span> url = <span class="md-code-keyword">new</span> URL(request.url);
  97. <span class="md-code-keyword">var</span> accepts = request.headers.get(<span class="md-code-string">&apos;Accept&apos;</span>);
  98. <span class="md-code-keyword">if</span> (accepts.indexOf(<span class="md-code-string">&apos;image&apos;</span>) !== -<span class="md-code-number">1</span>) {
  99. <span class="md-code-keyword">if</span> (url.host === <span class="md-code-string">&apos;www.gravatar.com&apos;</span>) {
  100. <span class="md-code-keyword">return</span> caches.match(mysteryMan);
  101. }
  102. <span class="md-code-keyword">return</span> caches.match(rainbows);
  103. }
  104. <span class="md-code-keyword">if</span> (url.origin === location.origin) {
  105. <span class="md-code-keyword">return</span> <mark class="md-mark md-code-mark">caches.match(<span class="md-code-string">&apos;/offline&apos;</span>)</mark>;
  106. }
  107. <span class="md-code-keyword">return</span> offlineResponse();
  108. }
  109. </code></pre><p>Lastly, since Pony Foo is a single page application, the ServiceWorker also needs to understand how to render an offline view using JSON. In that case we&#x2019;ll check that the <code class="md-code md-code-inline">origin</code> matches mine and that the headers are expecting <code class="md-code md-code-inline">application/json</code>. I can then construct a response that Taunus will interpret as the <code class="md-code md-code-inline">/offline</code> view.</p><pre class="md-code-block"><code class="md-code md-lang-javascript"><span class="md-code-keyword">if</span> (url.origin === location.origin &amp;&amp; accepts.indexOf(<span class="md-code-string">&apos;application/json&apos;</span>) !== -<span class="md-code-number">1</span>) {
  110. <span class="md-code-keyword">return</span> offlineView();
  111. }
  112. <span class="md-code-function"><span class="md-code-keyword">function</span> <span class="md-code-title">offlineView</span> <span class="md-code-params">()</span> </span>{
  113. <span class="md-code-keyword">var</span> viewModel = {
  114. model: { action: <span class="md-code-string">&apos;error/offline&apos;</span> }
  115. };
  116. <span class="md-code-keyword">var</span> options = {
  117. status: <span class="md-code-number">200</span>,
  118. headers: <span class="md-code-keyword">new</span> Headers({ <span class="md-code-string">&apos;content-type&apos;</span>: <span class="md-code-string">&apos;application/json&apos;</span> })
  119. };
  120. <span class="md-code-keyword">return</span> <span class="md-code-keyword">new</span> Response(<span class="md-code-built_in">JSON</span>.stringify(viewModel), options);
  121. }
  122. </code></pre><p>There&#x2019;s plenty of other options. In content sites you could go as far as to autogenerate a ServiceWorker file that has all of the content inlined in it <em>(or maybe in a big payload that&#x2019;s cached during installation)</em>. Or you could progressively crawl the site from the ServiceWorker using <code class="md-code md-code-inline">requestIdleCallback</code>. Or you could just cache things that humans actually visited. Most of the time, that&#x2019;s good enough.</p><p>As long as I&#x2019;m able to visit content I&#x2019;ve already seen, but offline, I&#x2019;m glad to have implemented ServiceWorker. The screenshot below shows <a href="http://WebPageTest.org">WebPageTest.org</a> results on repeat view where no requests are made whatsoever, shaving <code class="md-code md-code-inline">~200ms</code> from start render time and around <code class="md-code md-code-inline">~2.5s</code> from complete page load. Down <strong>from 43 requests to zero</strong>, and from <code class="md-code md-code-inline">~2mb</code> in page weight to <code class="md-code md-code-inline">~200kb</code>.</p><p><img alt="WebPageTest.org results on repeat view" data-src="https://i.imgur.com/lCH6mGU.png" class="js-only"><noscript><img src="https://i.imgur.com/lCH6mGU.png" alt="WebPageTest.org results on repeat view"></noscript></p><p>Definitely a worthwhile addition to any website.</p></section></section>