Harder, Better, Faster, Stronger
Supercharging Your Site with Service Workers
Stephen Fornal
Manager of Web Development
Tarrant County College, Fort Worth, TX
Agenda
- What is a service worker
- Service worker life cycle
- Fetch events & caching strategies
- Offline content
- Live demo
- Q & A
What is a Service Worker, Anyway
- Service workers are Javascript
- Run asynchronously “behind” your site
- Enable “cool stuff”
Some “Cool Stuff”
- Fetch API
- Cache API
- And other things
This is powerful!
A Good Analogy

A Bad Service Worker
- Be careful!
- A poorly written service worker will mess things up
- Only deploy to production when fully tested
How Do I Make a Service Worker
- Register the service worker
- A bit of javascript on your HTML pages
- The service worker script lives at the root of your site
Registering a Service Worker
/* Feature check */
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
/* It worked */
})
.catch(function(error) {
/* It didn't */
});
}
Promises, Promises
- APIs make extensive use of Promises
- Use then()/catch()
- Alternately write
async
functions and useawait
Life Cycle of a Service Worker
- Parsed
- Installing
- Installed / Waiting
- Activating
- Active
- Redundant
Parsed
- Syntactically correct Javascript
- Served from same origin
- Over HTTPS
Installing
- The
install
event fires - If the event handler fails, move to Redundant state
- Typical tasks include caching resources
Installed / Waiting
- The service worker is ready
- Any previous service worker remains in control until all clients close
- On a revisit, the new service worker moves to Activating
Activating
- The
activate
event fires - If the event handler fails, move to Redundant state
- Typical tasks include deleting old caches
Active
Your Service Worker is now in control of your site!
Redundant
- Installation or Activation event failed, or service worker was replaced by a new service worker
- A previously active service worker will resume control
What Can My Service Worker Do Now
- Listen for events!
- Very specifically, the
fetch
event - An opportunity to interrupt the browser when it gets a resource
Fetch Handling
/* In /sw.js at your site root */
self.addEventListener('fetch', function(event) {
var request = event.request;
event.respondWith( fetch(request) );
}); //end of listener
Fetch Handling Revised
If the user agent supports service workers, it also supports ES2015 syntax.
Safe to use arrow functions, const
and let
, etc.
self.addEventListener('fetch', event => {
const request = event.request
event.respondWith( fetch(request) )
})
Ok, How Do I Make That Useful
- Cache API allows creation of custom caches
- A service worker can store resources in the cache
- A trip to the cache is always faster
- Cached content is available offline
Cache First, Then Network
Here's a basic strategy:
- Check the cache
- If it's in the cache, return the cached copy
- If not, request the resource from the network
Fetch Handling Revised, Part 2
self.addEventListener('fetch', event => {
const request = event.request
event.respondWith(
caches.match(request) // Promise for a Response
.then(response => {
if (response) { // Annoying!
return response
}
else {
return fetch( request )
}
}) //End of then
) //End of respondWith
})
Pre-caching
- Use the
install
event to add resources to a cache - The Cache API allows creating and naming multiple caches
Setting Up
Define your caches…
// In your sw.js file
const VER = 'v1';
const ASSET_CACHE = 'assetCache_' + VER;
// Add other caches later
const CACHE_LIST = [ ASSET_CACHE ]
const FILES_TO_CACHE = [ '/css/style.css', '/js/core.js' ] // etc.
Install Event
Tell the service worker that it's not done installing until everything is cached
self.addEventListener('install', event => {
event.waitUntil( /* A Promise is fulfilled */ )
})
Install Event, Part 2
self.addEventListener('install', event => {
event.waitUntil(
caches.open(ASSET_CACHE)
.then(myCache => {
return myCache.addAll( FILES_TO_CACHE )
}) //end of open Promise
.then(() => self.skipWaiting()) // This is cool!
) //end of waitUntil
})
Cache Invalidation and Service Worker Updating
- Whenever a service worker file changes, the user agent will treat it as a new service worker
- Life cycle starts again
- Update service worker code when updating static assets like CSS and JS
Clean Up Your Old Caches
- During the
activate
event, delete the old caches - Leftovers from previous versions of the service worker
Activate Event
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys() // Returns a Promise
.then(cacheNames => {
return Promise.all(
cacheNames.map(name => {
if ( !CACHE_LIST.includes(name)) {
return caches.delete(name) // A Promise
}
}) //end of map()
) //end of Promise.all
}) // End of keys Promise
.then(() => clients.claim()) // This is cool!
) //end of waitUntil
})
Caching Strategies Revisited
Fine tune fetch
event handling to create a different strategy for images.
- If it's in the cache, return it
- Also, grab & cache a fresh copy from the network
- If it's not in the cache, get it from the network
- And cache it
Fetch Handling Revised, Part 4
// This is inside the fetch event listener
if (request.headers.get('Accept').includes('image')) {
fetchEvent.respondWith(
caches.match(request)
.then(response => {
if (response) { //It's in the cache!
fetchEvent.waitUntil(
fetch(request) // Grab a fresh copy
.then(fResponse => {
return
stash(IMG_CACHE, request, fResponse)
})
)
return response
}
Fetch Handling Revised, Part 5
Continued…
else {
return fetch(request)
.then(fResp => {
fetchEvent.waitUntil(
stash(IMG_CACHE, request, fResp.clone())
)
return fResp
})
}
})
)
}
Abstraction
// This is a utility function for a common task
// Returns a Promise
function stash(cacheName, request, response) {
caches.open(cacheName)
.then(cache => {
return cache.put(request, response)
})
}
Page Content
- Caching strategy for pages would be network first
- Pages change frequently
- If the network is unavailable, serve a cached copy
Page Caching Strategy
- Fetch the page from the network
- Put a copy in the cache
- If fetch fails, check the cache
- If the page doesn't exist in the cache ???
An Offline Experience
- Pre-cache an offline page
- TCC’s lives at
/offline/
- When the network and the cache fail show the offline page
- Lots of cool possibilities
Fetch Handling Revised, Part 6
// This is inside the event listener
if (request.headers.get('Accept').includes('text/html')) {
event.respondWith(
fetch(request)
.then(resp => { // Got it, so cache it
event.waitUntil(stash(PAGE_CACHE, request, resp.clone()))
return resp
})
.catch(() => { // Network failed, fall back to cache
caches.match(request)
.then(resp => {
return resp || caches.match('/offline/')
})
})
)
}
Let's See It In Action
Other Considerations
- Trim your caches
- Make the offline page useful
- Navigation Preloads
- Fallback Images
- Lie-Fi
A Quick PSA About PWAs
Persistent Web Applications require:
- A service worker
- Offline content
- File called a manifest
Resources
- Going Offline by Jeremy Keith
- The MDN docs for ServiceWorker, Fetch, and Cache APIs
- Chrome Dev Tools for debugging
Special Thanks to OmniUpdate
- Enter to win the all-new iPad® and Apple Pencil®!
- To enter, text HEW19 to 95323.
- Drawing sponsored by OmniUpdate.
Get a free website accessibility scan with results from OmniUpdate! try.omniupdate.com/scan-5