An Introduction To

Service Workers

Aaron Manire

amanire.github.io/isw

About myself

What is a Service Worker?

Photo by Niko Lienata on Unsplash

Web Workers API

w3.org/TR/workers/

An API for running scripts in the background independently of any user interface scripts
Workers are expected to be long-lived, have a high start-up performance cost, and a high per-instance memory cost

Types of Web Workers

  • Shared Workers
  • Audio Workers
  • Service Workers

Bounce rates

As page load time goes from:

  • 1s to 3s - probability of bounce ↑ 32%
  • 1s to 5s - probability of bounce ↑ 90%
  • 1s to 10s - probability of bounce ↑ 123%

Source: Google/SOASTA Research 2017

What Problems do Service Workers solve?

  • Offline experience
  • Weak connections
  • Reliance on strong continuous bandwidth
  • Especially relevant for phones

What Problems do Service Workers not solve?

  • First visit experience
  • Simplifying site maintenance

Typical Network Request

Failure and frustration

  • No nearby cell tower
  • Flaky Wifi
  • Traffic spikes, e.g a major event
  • ISP trouble
  • Power outage
  • Site server is down

Waiting isn't Easy

Credit: Mo Williams

Request with Service Worker

Fetch event


addEventListener('fetch', fetchEvent => {
	// If fetch is successful, return results.
	// …
	// If fetch fails, return cached content.
	// …
});
						

Requirements

Service Worker Lifecycle

First site visit

  1. Download
  2. Install
  3. Activate

Updated Service Worker

  1. Download
  2. Install
  3. Waiting
    Until all windows or tabs are closed.
  4. Activate

Working asynchronously: Promise API

A promise can either be fulfilled (resolved) or rejected.

developers.google.com/web/fundamentals/primers/promises

Creating a Promise


var promise = new Promise((resolve, reject) => {
	// Do something, e.g. fetch a file from the network.

	if (/* everything turned out fine */) {
		resolve("Stuff worked!");
	}
	else {
		reject(Error("It broke"));
	}
});
					

Using a Promise


promise
.then(result => {
	console.log(result); // "It worked!"
})
.catch(error => {
	console.log(error); // Error: "It broke."
});
						

Promises can be chained


promise
.then(result => {
	console.log(result); // "It worked!"
})
.then(data => {
	console.log(data); // "Do something afterwards."
})
.catch(error => {
	console.log(error); // Error: "It broke."
});
						

Promise-based APIs

  • Cache API
  • Fetch API

Cache API

Understanding Cache Lifetimes

Cache instances are not part of the browser’s HTTP cache…
Updates must be manually managed
authors should version their caches by name…
w3c.github.io/ServiceWorker/#cache-lifetimes

Browser Caching

  1. Memory cache
  2. Service Workers cache
  3. Disk (HTTP) cache

Caches property

(instance of CacheStorage)


caches.open(cacheName)
.then(cache => {
	// Do something with your cache
});
						
developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/caches

Cache methods

  • cache.add()
  • cache.put()
  • cache.match()
  • cache.keys()
  • cache.delete()

developer.mozilla.org/en-US/docs/Web/API/Cache

Add single offline HTML file to cache


caches.open(cacheName)
.then(cache => {
	return cache.add('offline.html');
})
						

Fetch API


fetch(request)
.then(responseFromFetch => {
	console.log(responseFromFetch); // "It fetched the thing!"
})
.catch(error => {
	console.log(error); // Error: "It failed."
});
							

Fetch user info from GitHub


fetch('https://api.github.com/users/amanire')
.then(response => {
	return response.json()
})
.then(data => {
	console.log(data);
})
.catch(err => {
	console.error(err);
})
							

Demo

Building a Service Worker

  • HTTPS (or localhost for development)
  • Register service worker
  • Service worker JavaScript file
  • Caching strategy

HTTPS


letsencrypt.org
certbot.eff.org

Register Service Worker

/index.html

via inline script tag


<script>
if ('serviceWorker' in navigator) {
	navigator.serviceWorker.register('/serviceworker.js');
}
</script>

No modern JavaScript here!

Progressive enhancement

Service worker file is ignored if unsupported by browser.

Scope


<script>
if (navigator.serviceWorker) {
	navigator.serviceWorker.register('/serviceworker.js',
	scope: '/app/'
	);
}
</script>

Service Worker file

/serviceworker.js

Versioned cache name


const version = 'V0.1';
const cacheName = 'MySWCache' + version;
						

Events


addEventListener('install', installEvent => {// …});

addEventListener('activate', activateEvent => {// …});

addEventListener('fetch', fetchEvent => {// …});
							

addEventListener('install', installEvent => {
	// Cache stuff for later
});

addEventListener('activate', activateEvent => {
	// Cleanup caches
});

addEventListener('fetch', fetchEvent => {
	// Return fetched or cached stuff
});
							

Install event


	addEventListener('install', installEvent => {
	// Cache stuff for later
	});
							

// cacheName == 'MySWCacheV0.1'
							

addEventListener('install', installEvent => {
	installEvent.waitUntil(
    caches.open(cacheName)
    .then(cache => {
      return cache.addAll([
        'offline.html',
        'styles.css'
      ]);
    })
  );
});
							

Activate event


addEventListener('activate', activateEvent => {
	// Cleanup caches
});
							

	// Delete old cacheName != 'MySWCacheV0.1'
								

addEventListener('activate', function (event) {
	activateEvent.waitUntil(
	caches.keys()
	.then(cacheNames => {
		return Promise.all(
			cacheNames.map( cacheNameKey => {
				if (cacheNameKey != cacheName) {
					return caches.delete(cacheNameKey);
				}
			})
		);
	})
	);
});
							

Fetch event

Fresh HTML, cached assets


addEventListener('fetch', fetchEvent => {
	// - Fetch HTML file from network
	//   If error, return offline fallback
	//
	// - Get all other assets from cache
	//   If not in cache, fetch from network
});
						

addEventListener('fetch', fetchEvent => {
	// Fetch HTML file from network
	const request = fetchEvent.request;
	if(request.headers.get('Accept').includes('text/html')) {
		fetchEvent.respondWith(
			fetch(request)
			.then(responseFromFetch => {
				return responseFromFetch;
			})
			// If error, return offline fallback
			.catch(error => {
				return caches.match('/offline.html');
			})
		);
		return;
	}
	// …
							

	// Get all other assets from cache
	fetchEvent.respondWith(
		caches.match(request)
		.then(responseFromCache => {
			if(responseFromCache) {
				return responseFromCache;
			}
			// If not in cache, fetch from network
			return fetch(request);
		})
	)
});
							

Caching strategy

Serve cached assets on time out


addEventListener('fetch', fetchEvent => {
// - If the network fails or the response is not served before timeout,
//   reject the network request and return the cached version.
//
// - Otherwise, fullfill the promise and
//   return fetched file from network
});
					
serviceworke.rs/strategy-network-or-cache.html

Caching strategy

Dynamic responsive image strategy


addEventListener('fetch', fetchEvent => {
	// Check if the image is a jpeg
	//   Inspect the accept header for WebP support
	//   If we support WebP
	//     Clone the request
	//     Build the return URL
});
		
deanhume.com/service-workers-dynamic-responsive-images-using-webp-images

							.addEventListener('fetch', function(event) {
								// Clone the request
								var req = event.request.clone();

								// Check if the image is a jpeg
								if (/\.jpg$|.png$/.test(event.request.url)) {

									// Get all of the headers
									let headers = Array.from(req.headers.entries());

									// Inspect the accept header for WebP support
									var acceptHeader = headers.find(item => item[0] == 'accept');
									var supportsWebp = acceptHeader[1].includes('webp');

									// If we support WebP
									if (supportsWebp) {
										// Build the return URL
										var returnUrl = req.url.substr(0, req.url.lastIndexOf(".")) + ".webp";

										event.respondWith(
											fetch(returnUrl, {
												mode: 'no-cors'
											})
										);
									}
								}
							});
					
deanhume.com/service-workers-dynamic-responsive-images-using-webp-images

Tips and Gotchas

Chrome developer tools

chromium.org/blink/serviceworker/service-worker-faq

Developer tools

Set console context to Service Worker

Don't wait for user to reload

Force the new waiting service worker to become the active service worker

davidwalsh.name/service-worker-claim

Add SkipWaiting() to Install event


addEventListener('install', event => {
	event.waitUntil(
		addEventListener('install', installEvent => {
			skipWaiting();
			installEvent.waitUntil(
				caches.open(cacheName)
				.then(cache => {
					return cache.addAll([
						'offline.html',
						'styles.css'
					]);
				})
	);
});
							

Chain clients.claim() to Activate event


									addEventListener('activate', function (event) {
										activateEvent.waitUntil(
										caches.keys()
										.then(cacheNames => {
											return Promise.all(
												cacheNames.map( cacheNameKey => {
													if (cacheNameKey != cacheName) {
														return caches.delete(cacheNameKey);
													}
												})
											);
										})
										.then( () => {
												clients.claim()
										})
										);
									});
							

Progressive Web Apps

  • Responsive
  • Connectivity independent
  • App-like-interactions
  • Fresh
  • Safe
  • Discoverable
  • Re-engageable
  • Installable
  • Linkable

Alew Russell & Frances Berriman
infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/

Minimum PWA

  • HTTPS
  • Service Worker
  • Manifest JSON file

Resources

Thank you

amanire.github.io/isw

Aaron Manire

@amanire

Feedback
nerd.ngo/feedback