Service Workers give developers the amazing ability to handle spotty networks and create truly offline-first web apps. But being a new technology means they can sometimes be difficult to debug, especially as we wait for our tools to catch up.

This codelab will walk you through creating a basic Service Worker and demonstrate how to use the new Application panel in Chrome DevTools to debug and inspect your worker.

What are we going to be building?

In this code lab you'll work with an extremely simple progressive web app and learn techniques you can employ in your own applications when you encounter issues.

Because this code lab is focused on teaching you tools, feel free to stop at various points and experiment. Play with the code, refresh the page, open new tabs, etc. The best way to learn debugging tools is just to break things and get your hands dirty fixing them.

What you'll learn

What you'll need

This codelab is focused on debugging Service Workers and assumes some prior knowledge of working with Service Workers. Some concepts are glossed over or code blocks (for example styles or non-relevant JavaScript) are provided for you to simply copy and paste. If you are new to Service Workers be sure to read through the API Primer before proceeding.

Download the Code

You can download all of the code for this codelab, by clicking the following button:

Download source code

Unpack the downloaded zip file. This will unpack a root folder (debugging-service-workers-master), which contains one folder for each step of this codelab, along with all of the resources you will need.

The step-NN folders contain the desired end state of each step of this codelab. They are there for reference. We'll be doing all our coding work in the directory called work.

Install and verify web server

While you're free to use your own web server, this codelab is designed to work well with the Chrome Web Server. If you don't have that app installed yet, you can install it from the Chrome Web Store.

Install Web Server for Chrome

After installing the Web Server for Chrome app, click on the Apps shortcut on the bookmarks bar:

In the ensuing window, click on the Web Server icon:

You'll see this dialog next, which allows you to configure your local web server:

Click the choose folder button, and select the work folder. This will enable you to serve your work in progress via the URL highlighted in the web server dialog (in the Web Server URL(s) section).

Under Options, check the box next to "Automatically show index.html", as shown below:

Then stop and restart the server by sliding the toggle labeled "Web Server: STARTED" to the left and then back to the right.

Now visit your work site in your web browser (by clicking on the highlighted Web Server URL) and you should see a page that looks like this:

Obviously, this app is not yet doing anything interesting. We'll add functionality so we can verify it works offline in subsequent steps.

Inspecting the Manifest

Building a Progressive Web Apps requires tying together a number of different core technologies, including Service Workers and Web App Manifests, as well as useful enabling technologies, like the Cache Storage API, IndexedDB, and Push Notifications. To make it easy for developers to get a coordinated view of each of these technologies the Chrome DevTools has incorporated inspectors for each in the new Application panel.

Look in the sidebar and notice Manifest is currently highlighted. This view shows important information related to the manifest.json file such as its application name, start URL, icons, etc.

Although we won't be covering it in this codelab, note that there is an Add to homescreen button which can be used to simulate the experience of adding the app to the user's homescreen.

Inspecting the Service Workers

In the past, inspecting a Service Worker required poking around in Chrome internals and was definitely not the most user friendly experience. All of that changes with the new Application tab!

The Service Workers view provides information about Service Workers which are active in the current origin. Along the top row there are a series of checkboxes.

Below that you will see information relating to the current active Service Worker (if there is one). One of the most useful fields is the Status field, which shows the current state of the Service Worker. Since this is the first time starting the app, the current Service Worker has successfully installed and been activated, so it displays a green circle to indicate everything's good.

Note the ID number next to the green status indicator. That's the ID for the currently active Service Worker. Remember it or write it down as we'll use it for a comparison in just a moment.

The code for the current Service Worker is quite simple, just a couple of console logs.

self.addEventListener('install', function(event) {
  console.log('Service Worker installing.');
});

self.addEventListener('activate', function(event) {
  console.log('Service Worker activating.');  
});

If you switch back to the DevTools and look in the Console you can see that both logs have been output successfully.

Let's update the code for the service-worker.js to watch it go through a lifecycle change.

self.addEventListener('install', function(event) {
  console.log('A *new* Service Worker is installing.');
});

self.addEventListener('activate', function(event) {
  console.log('Finally active. Ready to start serving content!');  
});

The console logs A *new* Service Worker is installing. but doesn't show the 2nd message about the new Service Worker being active.

In the Application tab there are now two status indicators, each representing the state of our two Service Workers.

Note the ID of the first Service Worker. It should match the original Service Worker ID. When you install a new Service Worker, the previous worker remains active until the next time the user visits the page.

The second status indicator shows the new Service Worker we just edited. Right now it's in a waiting state.

An easy way to force the new Service Worker to activate is with the skipWaiting button.

Note that the console now logs the message from the activate event handler:

Finally active. Ready to start serving content!

Managing your own offline file cache with a Service Worker is an incredible super power. The new Application panel has a number of useful tools for exploring and modifying your stored resources which can be very helpful during development time.

Add caching to your Service Worker

Before you can inspect the cache you'll need to write a little code to store some files. Pre-caching files during the Service Worker's install phase is a useful technique to guarantee that crucial resources are available to user if they happen to go offline. Let's start there.

This useful trick will force the page to use whatever Service Worker is the latest, so you don't have to click the skipWaiting option every time you want to make changes to your Service Worker.

var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/smiley.svg'
];

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        return cache.addAll(urlsToCache);
      })
  );  
});

self.addEventListener('activate', function(event) {
  console.log('Finally active. Ready to start serving content!');  
});

In the Application panel you might notice an Error shows up. This seems scary but clicking the details button reveals that it's just the Application panel telling you that your old Service Worker was forcibly updated. Since that was the intention, this is totally O.K., but it can serve as a useful warning so you don't forget to turn the checkbox off when you're done editing the service-worker.js file.

Inspecting Cache Storage

Notice that the Cache Storage menu item in the Application panel now has a caret indicating it can be expanded.

Here you can see all of the files cached by the Service Worker. If you need to remove a file from the cache you can right-click on it and select the delete option from the context menu. Similarly, you can delete the entire cache by right-clicking on my-site-cache-v1 and choosing delete.

Cleaning the slate

As you may have noticed, along with Cache Storage, there are a number of other menu items related to stored resources, including: Local Storage, Session Storage, IndexedDB, Web SQL, Cookies, and Application Cache ("AppCache"). Having granular control of each of these resources all in one panel is extremely useful! But if you were in a scenario where you wanted to delete all of the stored resources it would be pretty tedious to have to visit each menu item and delete their contents. Instead, you can use the Clear storage option to clean the slate in one fell swoop (note that this will also unregister any Service Workers).

If you go back and click on my-site-cache-v1 you'll now see that all the stored files have been deleted.

What's with the gear?

Because the Service Worker is able to make its own network requests, it can be useful to identify network traffic which originated from the worker itself.

In the Network panel, you should see an initial set of request for files like main.css, followed by a second round of requests, prefixed with a gear icon, which seem to fetch the same assets.

The gear icon signifies that these requests came from the Service Worker itself. Specifically, these are the requests being made by the Service Worker's install handler to populate the offline cache.

One of the killer feature of Service Workers is their ability to serve cached content to users even when they're offline. To verify everything works as planned, let's test out some of the network throttling tools that Chrome provides.

Serving requests while offline

In order to serve offline content, you'll need to add a fetch handler to your service-worker.js

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
    )
  );
});

Your Application panel should look like this now:

Notice the Network panel now has a yellow warning sign to indicate that you're offline (and to remind you that you'll want to uncheck that checkbox if you want to continue developing with the network).

With your fetch handler in place, and your app set to Offline, now is the moment of truth. Refresh the page and if all goes well you should continue to see site content, even though nothing is coming from the network. You can switch to the Network panel to verify that all of the resources are being served from Cache Storage. Notice in the Size column it says these resources are coming (from Service Worker). That's the signal that tells us the Service Worker intercepted the request, and served a response from the cache instead of hitting the network.

You'll notice that there are failed requests (like for a new Service Worker or manifest.json). That's totally fine and expected.

Testing slow or flakey networks

Because we user our mobile devices in a plethora of different contexts, we're constantly moving between various states of connectivity. Not only that but there are many parts of the world where 3G and 2G speeds are the norm. To verify our app works well for these consumers we should test that it is performant even on a slower connection.

To start, let's simulate how the application works on a slow network when the Service Worker is not in play.

The Bypass for network option will tell the browser to skip our service worker when it needs to make a network request. This means nothing will be able to come from Cache Storage, it will be as if we have no Service Worker installed at all.

The Network Throttle dropdown is located in the top right of the Network panel, right next to the Network panel's own Offline checkbox. By default it is set to No throttling.

Notice the response times jump way up! Now each asset takes several hundred milliseconds to download.

Let's see how things differ with our Service Worker back in play.

Now our response times jump down to a blazing fast few milliseconds per resource. For users on slower networks this is a night and day difference!

Service Workers can feel like magic, but under the hood they're really just regular JavaScript files. This means you can use existing tools like debugger statements and breakpoints to debug them.

Working with the debugger

Many developers rely on good old console.log() when they have an issue in their app. But there's a much more powerful tool available in the toolbox: debugger.

Adding this one line to your code will pause execution and open up the Sources panel in the DevTools. From here you can step through functions, inspect objects, and even use the console to run commands against the current scope. This can be especially useful for debugging a cranky Service Worker.

To test it out, let's debug our install handler.

self.addEventListener('install', function(event) {
  debugger;
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        return cache.addAll(urlsToCache);
      })
  );  
});

The application will pause execution and switch panels over to Sources where the debugger statement is now highlighted in service-worker.js.

There are a ton of useful tools available in this view. One such tool is the Scope inspector, which let's us see the current state of objects in the current function's scope.

From here you can learn all sorts of useful information about the current in-scope objects. For instance, looking at the type field you can verify that the current event object is for the install event.

Using breakpoints instead

If you're already inspecting your code in the Sources panel, you may find it easier to set a breakpoint, versus adding debugger statements to your actual files. A breakpoint serves a similar purpose (it freezes execution and let's you inspect the app) but it can be set from within DevTools itself.

To set a breakpoint you need to click the line number where you'd like the application to halt execution.

This will set a breakpoint at the beginning of the fetch handler so you can inspect its event object.

Notice that, similar to when you used the debugger statement, execution has now stopped on the line with the breakpoint. This means you can now inspect the FetchEvent objects passing through your app and determine what resources they were requesting.

You can see that this FetchEvent was requesting the resource at http://127.0.0.1:8887/, which is our index.html. Because the app will handle many fetch requests, you can leave the breakpoint in place and resume execution. This will let you inspect each FetchEvent as it passes through the app. A very useful technique for getting a fine grained look at all the requests in your app.

After a moment, execution will pause on the same breakpoint. Check the event.request.url property and note it now displays http://127.0.0.1:8887/styles/main.css. You can continue in this way to watch it request smiley.svg, main.js, and finally the manifest.json.

Push notifications are an important part of creating an engaging experience. Because notifications require coordination between an application server, a messaging service (like Google Cloud Messaging), and your Service Worker, it can be useful to test the Service Worker in isolation first to verify it is setup properly.

Adding Push support

You may have noticed a button in the center of the application asking for the user to Subscribe for Push Notifications. This button is already wired up to request the Push notification permission from the user when clicked.

The only remaining step is to add support for the push event to service-worker.js.

self.addEventListener('push', function(event) {  
  var title = 'Yay a message.';  
  var body = 'We have received a push message.';  
  var icon = '/images/smiley.svg';  
  var tag = 'simple-push-example-tag';
  event.waitUntil(  
    self.registration.showNotification(title, {  
      body: body,  
      icon: icon,  
      tag: tag  
    })  
  );  
});

With the handler in place it's easy to simulate a Push event.

You should now see a Push notification appear in the top right of the screen, confirming that the Service Worker is handling push events as expected.

Nice work!

Now that you have some debugging tools in your toolbox, you should be well equipped to fix-up any issues that arise in your project. The only thing left is for you to get out there and build the next amazing Progressive Web App!