Service Workers

Spoiler: server-rendered HTML can work offline

The architecture astronauts who, for the past decade, have been selling us on the necessity of React, Redux, and megabytes of JS, cannot comprehend the possibility of building an email app in 2020 with server-rendered HTML 😴


The effects are truly toxic. Last decade’s obsession with SPAs has poisoned the minds of even the brightest teachers in our industry.

Like, there’s no way this stuff can work offline, right?!

Briefly read up on how HEY is implemented. Is this a correct summary of the pros and cons of its sever-centric approach?
– Pro: Works fast on older devices.
– Con: Can’t be used offline.

Rethinking Offline First sync for Service Workers

On the surface, Service Workers look quite similar to Web Workers. They both run on separate threads from the main UI thread, they have a global self object, and they tend to support the same APIs. However, while Web Workers offer a large degree of control over their lifecycle (you can create and terminate them at will) and are able to execute long-running JavaScript tasks (in fact, they’re designed for this), Service Workers explicitly don’t allow either of these things. In fact, a Service Worker is best thought of as “fire-and-forget” — it responds to events in an ephemeral way, and the browser is free to terminate any Service Worker that takes too long to fulfill a request or makes too much use of shared resources.

This led us to our first real hurdle with Service Worker. Our goal, as we originally conceived it, was to use PouchDB’s existing replication APIs to enable bi-directional sync between the client and the server, with the client code isolated entirely to the Service Worker.


This resulted in a silent error, which took quite a while to debug. The culprit? Well, PouchDB’s “live” sync depends on HTTP longpolling — in other words, it maintains an ongoing HTTP connection with the CouchDB server, which is used to send real-time updates from the server to the client. As it turns out, this is a big no-no in Service Worker Land, and the browser will unceremoniously drop your Service Worker if it tries to maintain any ongoing HTTP connections. The same applies to Web Sockets, Server-Sent Events, WebRTC, and any other network APIs where you may be tempted to keep a constant connection with your server.

What we realized is that “the Zen of Service Worker” is all about embracing events. The Service Worker receives events, it responds to events, and it (ideally) does so in a timely manner — if not, the browser may preemptively terminate it. And this is actually a good design decision in the spec, since it prevents malicious websites from installing rogue Service Workers that abuse the user’s battery, memory, or CPU.

When 7 KB Equals 7 MB

While testing a progressive web app for one of our clients, I bumped into a suspicious error in the browser console:

DOMException: Quota exceeded.

After browsing the app a few more times, it became clear the error would occur after a small number of images were added to the cache storage by the service worker. Looking in the Chrome DevTools Application tab, the cache storage was indeed over capacity.

Chrome DevTools showing over-capacity cache storage.
Chrome DevTools showing over-capacity cache storage.

How could this be? There were only ~15 images in the cache storage. Something was off.

What I found could significantly impact your progressive web app—particularly if you use a CDN on a different domain for your assets.


If you are building a progressive web app and are experiencing bloated cache storage when your service worker caches static assets served from CDNs, make sure the proper CORS response header exists for cross-origin resources, you do not cache opaque responses with your service worker unintentionally, you opt-in cross-origin image assets into CORS mode by adding the crossorigin attribute to the <img> tag.

See the source link for more details.

Network based image loading using the Network Information API in Service Worker

Recently, Chromium improved their implementation of navigator.connection by adding three new attributes: effectiveType, downlink and rtt.

Before that, the available attributes were downLinkMax and type. With these two attributes you couldn’t really tell if the connection was fast or slow. The navigator.connection.type may tell us a user is using WiFi, but this doesn’t say anything about the real connection speed, as they may be using a hot spot and the connection is in fact 2G.

With the addition of effectiveType we are finally able to get the real connection type. There are four different types (slow-2g, 2g, 3g and 4g) and they are described this way by the Web Incubator Community Group:

slow-2g: The network is suited for small transfers only such as text-only pages.
2g: The network is suited for transfers of small images.
3g: The network is suited for transfers of large assets such as high resolution images, audio, and SD video.
4g: The network is suited for HD video, real-time video, etc.

Let’s see how we can improve user experience by delivering images based on available connection speed.

Send messages when you're back online with Service Workers and Background Sync

This example of using background sync looks like it’s specific to Twilio, but the breakdown of steps is broad enough to apply to many situations:

On the page we need to:

  1. Register a Service Worker
  2. Intercept the “submit” event for our message form
  3. Place the message details into IndexedDB, an in browser database
  4. Register the Service Worker to receive a “sync” event

Then, in the Service Worker we need to:

  1. Listen for sync events
  2. When a sync event is received, retrieve the messages from IndexedDB
  3. For each message, send a request to our server to send the message
  4. If the message is sent successfully, then remove the message from IndexedDB

And that’s it.

Async events in ServiceWorkers with "event.waitUntil"

It’s asynchronous, see? So even though all that network code appears before the return statement, it’s pretty much guaranteed to complete after the cache response has been returned. You can verify this by putting in some console.log statements:

Code language: JavaScript

.then( responseFromCache => {
  if (responseFromCache) {
          .then( responseFromFetch => {
              console.log('Got a response from the network.');
              .then( cache => {
                  cache.put(request, responseFromFetch);
      console.log('Got a response from the cache.');
      return responseFromCache;

Those log statements will appear in this order:

Got a response from the cache.
Got a response from the network.

That’s the opposite order in which they appear in the code. Everything inside the event.waitUntil part is asynchronous.

Here’s the catch: this kind of asynchronous waitUntil hasn’t landed in all the browsers yet. The code I’ve written will fail.

But never fear! Jake has written a polyfill. All I need to do is include that at the start of my serviceworker.js file and I’m good to go:

Code language: JavaScript

// Import Jake's polyfill for async waitUntil