A service worker is a JavaScript Worker script that your browser runs in the background, separate from a web page, opening the door to features that don’t need a web page or user interaction. Today, they already include features like push notifications and background sync
Service Worker’s Core Feature
The core feature discussed in this tutorial is the ability to intercept and handle network requests, including programmatically managing a cache of responses
Prerequisite Reading
Things to Note About a Service Worker
- service worker is a JavaScript Worker, so it can’t access the DOM directly. Instead, a service worker can communicate with the pages it controls by responding to messages sent via the postMessage interface, and those pages can manipulate the DOM if needed.
- service worker is a programmable network proxy, allowing you to control how network requests from your page are handled.
- service worker is terminated when not in use, and restarted when it’s next needed, so you cannot rely on global state within a service worker’s
onfetchandonmessagehandlers. If there is information that you need to persist and reuse across restarts, service workers do have access to the IndexedDB API. - service workers make extensive use of promises
Service Worker Prerequisites
Click here to expand...
Browser support
Browser options are growing. Service workers are supported by Chrome, Firefox and Opera. Microsoft Edge is now showing public support. Even Safari has dropped hints of future development. You can follow the progress of all the browsers at Jake Archibald’s is Serviceworker ready site.
You need HTTPS
During development you’ll be able to use service worker through
localhost, but to deploy it on a site you’ll need to have HTTPS setup on your server.Using service worker you can hijack connections, fabricate, and filter responses. Powerful stuff. While you would use these powers for good, a man-in-the-middle might not. To avoid this, you can only register service workers on pages served over HTTPS, so we know the service worker the browser receives hasn’t been tampered with during its journey through the network.
GitHub Pages are served over HTTPS, so they’re a great place to host demos.
If you want to add HTTPS to your server then you’ll need to get a TLS certificate and set it up for your server. This varies depending on your setup, so check your server’s documentation and be sure to check out Mozilla’s SSL config generator for best practices.
Service Worker Life Cycle

1. Register a Service Worker
Click here to expand...
This tells the browser where your service worker JavaScript file lives
if (‘serviceWorker’ in navigator) {window.addEventListener(‘load’, function() {navigator.serviceWorker.register(‘/sw.js’).then(function(registration) {// Registration was successfulconsole.log(‘ServiceWorker registration successful with scope: ’, registration.scope);}, function(err) {// registration failed :(console.log(‘ServiceWorker registration failed: ’, err);});});}This code checks to see if the service worker API is available, and if it is, the service worker at
/sw.jsis registered once the page is loaded.You can call
register()every time a page loads without concern; the browser will figure out if the service worker is already registered or not and handle it accordingly.One subtlety with the
register()method is the location of the service worker file. You’ll notice in this case that the service worker file is at the root of the domain. This means that the service worker’s scope will be the entire origin. In other words, this service worker will receivefetchevents for everything on this domain. If we register the service worker file at/example/sw.js, then the service worker would only seefetchevents for pages whose URL starts with/example/(i.e./example/page1/,/example/page2/).Now you can check that a service worker is enabled by going to
chrome://inspect/#service-workersand looking for your site.
after a service worker is registered the browser installs it
2. Install Event
Click here to expand...
After a controlled page kicks off the registration process, let’s shift to the point of view of the service worker script, which handles the
installevent.For the most basic example, you need to define a callback for the install event and decide which files you want to cache.
self.addEventListener(‘install’, function(event) {// Perform install steps});Inside of our
installcallback, we need to take the following steps:
open a cache
cache our files
confirm whether all the required assets are cached or not
var CACHE_NAME = ‘my-site-cache-v1’;var urlsToCache = [’/’,‘/styles/main.css’,‘/script/main.js’];self.addEventListener(‘install’, function(event) {// Perform install stepsevent.waitUntil(caches.open(CACHE_NAME).then(function(cache) {console.log(‘Opened cache’);return cache.addAll(urlsToCache);}));});Here you can see we call
caches.open()with our desired cache name, after which we callcache.addAll()and pass in our array of files. This is a chain of promises (caches.open()andcache.addAll()). Theevent.waitUntil()method takes a promise and uses it to know how long installation takes, and whether it succeeded or not.If all the files are successfully cached, then the service worker will be installed. If any of the files fail to download, then the install step will fail. This allows you to rely on having all the assets that you defined, but does mean you need to be careful with the list of files you decide to cache in the install step. Defining a long list of files will increase the chance that one file may fail to cache, leading to your service worker not getting installed.
This is just one example, you can perform other tasks in the
installevent or avoid setting aninstallevent listener altogether.
After a service worker is installed and the user navigates to a different page or refreshes, the service worker will begin to receive fetch events, an example of which is below (a service worker would only see fetch events based on how the service worker was registered)
3. Fetch Event
Click here to expand...
self.addEventListener(‘fetch’, function(event) {event.respondWith(caches.match(event.request).then(function(response) {// Cache hit - return responseif (response) {return response;}return fetch(event.request);}));});Here we’ve defined our
fetchevent and withinevent.respondWith(), we pass in a promise fromcaches.match(). This method looks at the request and finds any cached results from any of the caches your service worker created.If we have a matching response, we return the cached value, otherwise we return the result of a call to
fetch, which will make a network request and return the data if anything can be retrieved from the network. This is a simple example and uses any cached assets we cached during the install step.If we want to cache new requests cumulatively, we can do so by handling the response of the fetch request and then adding it to the cache, like below.
self.addEventListener(‘fetch’, function(event) {event.respondWith(caches.match(event.request).then(function(response) {// Cache hit - return responseif (response) {return response;}return fetch(event.request).then(function(response) {// Check if we received a valid responseif(!response || response.status ! 200 || response.type ! ‘basic’) {return response;}// IMPORTANT: Clone the response. A response is a stream// and because we want the browser to consume the response// as well as the cache consuming the response, we need// to clone it so we have two streams.var responseToCache = response.clone();caches.open(CACHE_NAME).then(function(cache) {cache.put(event.request, responseToCache);});return response;});}));});What we are doing is this:
- Add a callback to
.then()on thefetchrequest.- Once we get a response, we perform the following checks:
- Ensure the response is valid.
- Check the status is
200on the response.- Make sure the response type is basic, which indicates that it’s a request from our origin. This means that requests to third party assets aren’t cached as well.
- If we pass the checks, we clone the response. The reason for this is that because the response is a Stream, the body can only be consumed once. Since we want to return the response for the browser to use, as well as pass it to the cache to use, we need to clone it so we can send one to the browser and one to the cache.
4. Activate Event (When New Service Worker Replaces Old One)
Click here to expand...
There will be a point in time where your service worker will need updating. When that time comes, you’ll need to follow these steps:
- Update your service worker JavaScript file. When the user navigates to your site, the browser tries to redownload the script file that defined the service worker in the background. If there is even a byte’s difference in the service worker file compared to what it currently has, it considers it new.
- Your new service worker will be started and the
installevent will be fired.- At this point the old service worker is still controlling the current pages so the new service worker will enter a
waitingstate.- When the currently open pages of your site are closed, the old service worker will be killed and the new service worker will take control.
- Once your new service worker takes control, its
activateevent will be fired.One common task that will occur in the
activatecallback is cache management. The reason you’ll want to do this in theactivatecallback is because if you were to wipe out any old caches in the install step, any old service worker, which keeps control of all the current pages, will suddenly stop being able to serve files from that cache.Let’s say we have one cache called
‘my-site-cache-v1’, and we find that we want to split this out into one cache for pages and one cache for blog posts. This means in the install step we’d create two caches,‘pages-cache-v1’and‘blog-posts-cache-v1’and in the activate step we’d want to delete our older‘my-site-cache-v1’.The following code would do this by looping through all of the caches in the service worker and deleting any caches that aren’t defined in the cache whitelist.
self.addEventListener(‘activate’, function(event) {var cacheWhitelist = [‘pages-cache-v1’, ‘blog-posts-cache-v1’];event.waitUntil(caches.keys().then(function(cacheNames) {return Promise.all(cacheNames.map(function(cacheName) {if (cacheWhitelist.indexOf(cacheName) === -1) {return caches.delete(cacheName);}}));}));});
5. Putting It All Together
Click here to expand...
live example: https://googlechrome.github.io/samples/service-worker/basic/
JavaScript Snippet To Register Service Worker
if (‘serviceWorker’ in navigator) {window.addEventListener(‘load’, function() {navigator.serviceWorker.register(‘service-worker.js’).then(function(registration) {// Registration was successfulconsole.log(‘ServiceWorker registration successful with scope: ’, registration.scope);}, function(err) {// registration failed :(console.log(‘ServiceWorker registration failed: ’, err);});});}service-worker.js
// Names of the two caches used in this version of the service worker.// Change to v2, etc. when you update any of the local resources, which will// in turn trigger the install event again.const PRECACHE = ‘precache-v1’;const RUNTIME = ‘runtime’;// A list of local resources we always want to be cached.const PRECACHE_URLS = [‘index.html’,’./’, // Alias for index.html’styles.css’,’../../styles/main.css’,‘demo.js’];// The install handler takes care of precaching the resources we always need.self.addEventListener(‘install’, event => {event.waitUntil(caches.open(PRECACHE).then(cache => cache.addAll(PRECACHE_URLS)).then(self.skipWaiting()));});// The activate handler takes care of cleaning up old caches.self.addEventListener(‘activate’, event => {const currentCaches = [PRECACHE, RUNTIME];event.waitUntil(caches.keys().then(cacheNames => {return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));}).then(cachesToDelete => {return Promise.all(cachesToDelete.map(cacheToDelete => {return caches.delete(cacheToDelete);}));}).then(() => self.clients.claim()));});// The fetch handler serves responses for same-origin resources from a cache.// If no response is found, it populates the runtime cache with the response// from the network before returning it to the page.self.addEventListener(‘fetch’, event => {// Skip cross-origin requests, like those for Google Analytics.if (event.request.url.startsWith(self.location.origin)) {event.respondWith(caches.match(event.request).then(cachedResponse => {if (cachedResponse) {return cachedResponse;}return caches.open(RUNTIME).then(cache => {return fetch(event.request).then(response => {// Put a copy of the response in the runtime cache.return cache.put(event.request, response.clone()).then(() => {return response;});});});}));}});