What are Service Workers? The Ultimate Guide for PWAs

Service workers are a type of web worker, which are scripts that run in the background of the browser, separate from the main thread. Service workers act as a network proxy, intercepting and handling requests made by the web pages or apps they are associated with. Service workers can also access the Cache and IndexedDB APIs, allowing them to store and retrieve data locally.

Service workers are a core component of Progressive Web Apps (PWAs), which are web applications that provide a native-like user experience, with features such as offline functionality, push notifications, and home screen installation. Service workers enable PWAs to work offline or on low-quality networks, by serving cached resources and custom responses. Service workers also allow PWAs to receive push notifications and background sync events, even when the app is not running.

How do Service Workers Work?

Service workers have a specific life cycle, which consists of the following steps:

Service worker life cycle infographic

Registration:

The first step is to register a service worker in the web page or app using the navigator.serviceWorker.register() method. This method takes the URL of the service worker script as an argument, and returns a promise that resolves to a ServiceWorkerRegistration object. The registration object contains information about the service worker, such as its scope and status. The scope defines the set of URLs that the service worker can control, and is usually the same as the directory where the service worker script is located.

JAVASCRIPT
                        
// Register a service worker with the default scope
navigator.serviceWorker.register('/sw.js')
  .then(function(registration) {
    // Registration was successful
  console.log('ServiceWorker registration successful with scope: ', registration.scope);
  })
  .catch(function(err) {
    // Registration failed
    console.log('ServiceWorker registration failed: ', err);
  });

Installation:

After the registration, the browser downloads the service worker script and attempts to install it. The installation process involves running the service worker for the first time and executing the install event handler. The install event handler is where the service worker can perform tasks such as caching static assets, creating databases, or setting up initial state. The service worker can use the event.waitUntil() method to extend the lifetime of the install event until the tasks are completed. The service worker can also use the self.skipWaiting() method to skip the waiting phase and activate immediately.

JAVASCRIPT
                        
// Listen for the install event
self.addEventListener('install', function(event) {
  // Perform tasks such as caching static assets
  event.waitUntil(
    caches.open('my-cache')
      .then(function(cache) {
        // Cache some files
        return cache.addAll([
          '/',
          '/index.html',
          '/style.css',
          '/script.js',
          '/logo.png'
        ]);
      })
  );
  // Skip the waiting phase and activate immediately
  self.skipWaiting();
});

Activation:

After the installation, the service worker enters the activation phase. The activation process involves running the service worker again and executing the activate event handler. The activate event handler is where the service worker can perform tasks such as deleting old caches, updating databases, or claiming clients. The service worker can use the event.waitUntil() method to extend the lifetime of the activate event until the tasks are completed. The service worker can also use the self.clients.claim() method to take control of all the clients under its scope.

JAVASCRIPT
                        
// Listen for the activate event
self.addEventListener('activate', function(event) {
  // Perform tasks such as deleting old caches
  event.waitUntil(
    caches.keys()
      .then(function(cacheNames) {
        // Delete all the caches except the current one
        return Promise.all(
          cacheNames.map(function(cacheName) {
            if (cacheName !== 'my-cache') {
              return caches.delete(cacheName);
            }
          })
        );
      })
  );
  // Take control of all the clients under the scope
  self.clients.claim();
});

Fetching:

After the activation, the service worker is ready to handle fetch events, which are triggered by any network requests made by the clients under its scope. The fetch events are handled by the fetch event handler, which receives a FetchEvent object as an argument. The FetchEvent object contains the request information, such as the URL, method, headers, and body. The fetch event handler can respond to the fetch events by using the event.respondWith() method, which takes a promise that resolves to a Response object. The Response object represents the response to the request, and can be created from different sources, such as the network, the cache, or a custom response.

JAVASCRIPT
                        
// Listen for the fetch event
self.addEventListener('fetch', function(event) {
  // Respond to the fetch events
  event.respondWith(
    // Try to fetch the request from the network
    fetch(event.request)
      .then(function(response) {
        // If successful, return the network response
        return response;
      })
      .catch(function(error) {
        // If failed, try to fetch the request from the cache
        return caches.match(event.request)
          .then(function(response) {
            // If found, return the cached response
            if (response) {
              return response;
            }
            // If not found, return a custom response
            return new Response('Sorry, you are offline');
          });
      })
  );
});

How to Use Service Workers?

Service workers can be used for various purposes, such as:

  • Offline functionality: Service workers can provide offline functionality by caching the app's resources and serving them from the cache when the network is unavailable. This can improve the app's performance and reliability, as well as the user's experience. To implement offline functionality, the service worker needs to cache the app's resources during the installation phase, and respond to the fetch events with the cached resources during the fetching phase. The service worker can also use different caching strategies, such as cache-first, network-first, stale-while-revalidate, or cache-only, depending on the app's requirements and preferences.⁵
  • Push notifications: Service workers can receive push notifications from the server, even when the app is not running. Push notifications are messages that are sent to the user's device by the server, and can contain information such as news, updates, offers, or reminders. To receive push notifications, the service worker needs to subscribe to a push service, which is a third-party service that handles the delivery of push messages. The service worker also needs to listen for the push event, which is triggered when a push message arrives. The push event handler can display the push message as a notification, using the self.registration.showNotification() method, which takes a title and an options object as arguments. The options object can contain information such as the body, icon, badge, sound, vibration, or actions of the notification.
JAVASCRIPT
                        
// Subscribe to a push service
self.registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: 'some-key'
})
  .then(function(subscription) {
    // Subscription was successful
    console.log('Push subscription: ', subscription);
  })
  .catch(function(error) {
    // Subscription failed
    console.log('Push subscription failed: ', error);
  });

// Listen for the push event
self.addEventListener('push', function(event) {
  // Display the push message as a notification
  event.waitUntil(
    self.registration.showNotification('Hello', {
      body: 'You have a new message',
      icon: '/logo.png',
      badge: '/badge.png',
      sound: '/sound.mp3',
      vibrate: [200, 100, 200],
      actions: [
        {action: 'reply', title: 'Reply'},
        {action: 'dismiss', title: 'Dismiss'}
      ]
    })
  );
});
  • Background sync: Service workers can perform background sync, which is the ability to synchronize data between the app and the server, even when the app is not running. Background sync can be useful for scenarios such as sending messages, uploading files, or updating data. To perform background sync, the service worker needs to register a sync event, which is a one-off or periodic event that is triggered when the network is available. The service worker also needs to listen for the sync event, which is triggered when the sync event is ready to fire. The sync event handler can perform the synchronization tasks, such as sending requests, receiving responses, or updating the app's state.
JAVASCRIPT
                        
// Register a sync event
self.registration.sync.register('my-sync')
  .then(function() {
    // Registration was successful
    console.log('Sync registration successful');
  })
  .catch(function(error) {
    // Registration failed
    console.log('Sync registration failed: ', error);
  });

// Listen for the sync event
self.addEventListener('sync', function(event) {
  // Perform the synchronization tasks
  if (event.tag === 'my-sync') {
    event.waitUntil(
      // Send a request to the server
      fetch('/sync')
        .then(function(response) {
          // Receive a response from the server
          return response.json();
        })
        .then(function(data) {
          // Update the app's state with the data
          // ...
        });
      })
  );
});

How to Debug Service Workers?

Service workers can be debugged using the browser's developer tools, which provide various features and options to inspect and manipulate the service worker's behavior and state. Some of the common debugging tasks are:

  • Viewing the registered service workers: The browser's developer tools usually have a dedicated section or tab for service workers, where you can see the list of registered service workers, their status, scope, and source. You can also perform actions such as unregistering, updating, or stopping the service workers.
  • Viewing the cached resources: The browser's developer tools also have a section or tab for the Cache Storage API, where you can see the list of caches created by the service workers, and the resources stored in each cache. You can also perform actions such as adding, deleting, or refreshing the caches and the resources.
  • Viewing the network requests and responses: The browser's developer tools have a network panel, where you can see the details of the network requests and responses handled by the service workers, such as the URL, method, status, headers, and body. You can also filter the requests by type, status, or source.
  • Viewing the console logs and errors: The browser's developer tools have a console panel, where you can see the logs and errors generated by the service workers, such as the messages, warnings, or exceptions. You can also use the console object to log custom messages or values from the service worker's code.
  • Viewing the push notifications and background sync events: The browser's developer tools have a section or tab for the Push and Sync APIs, where you can see the details of the push notifications and background sync events received by the service workers, such as the payload, tag, or timestamp. You can also perform actions such as sending a test push message or triggering a sync event.
  • Setting breakpoints and stepping through the code: The browser's developer tools have a sources panel, where you can see the source code of the service workers, and set breakpoints to pause the execution and inspect the variables and the call stack. You can also use the debugger statement to programmatically trigger a breakpoint from the service worker's code. You can also use the step over, step into, step out, and resume buttons to control the execution flow.

Limitations of Service Workers

Service workers are a powerful and versatile tool for web development, but they also have some limitations and challenges that need to be considered. Some of these limitations are:

  • Browser support: Service workers are not supported by all browsers, especially older or legacy browsers. This means that some users may not be able to access the features and benefits of service workers, such as offline functionality, push notifications, or background sync. To ensure compatibility and fallback options, developers need to use feature detection and progressive enhancement techniques, such as checking the availability of the navigator.serviceWorker object, and providing alternative solutions for unsupported browsers.
  • Scope restriction: Service workers can only control the requests and responses within their scope, which is usually the same as the directory where the service worker script is located. This means that service workers cannot access or manipulate requests and responses from other origins or domains, unless they use CORS (Cross-Origin Resource Sharing) or other techniques to enable cross-origin communication. This also means that service workers cannot control the requests and responses from the parent or sibling directories, unless they use the scope option in the register() method to specify a broader scope.
  • Update delay: Service workers have a complex update mechanism, which involves checking for updates, installing the new version, waiting for the old version to become redundant, and activating the new version. This means that service workers may not reflect the latest changes or updates in the app's code or resources, unless the update process is completed and the new version is activated. To ensure that the service workers are updated as soon as possible, developers need to use strategies such as calling the update() method on the registration object, using the self.skipWaiting() method in the install event handler, or using the self.clients.claim() method in the activate event handler.
  • Debugging difficulty: Service workers can be difficult to debug, as they run in the background, separate from the main thread, and have a different life cycle and behavior than regular web pages or apps. This means that service workers may not behave as expected, or may cause errors or conflicts that are hard to identify or resolve. To debug service workers, developers need to use the browser's developer tools, which provide various features and options to inspect and manipulate the service worker's state and behavior, such as viewing the registered service workers, the cached resources, the network requests and responses, the console logs and errors, the push notifications and background sync events, setting breakpoints and stepping through the code, and performing actions such as unregistering, updating, or stopping the service workers.

Conclusion

Service workers are a type of web worker that run in the background of the browser and manage caching and network requests for the web pages or apps they are associated with. Service workers have a specific life cycle, which consists of registration, installation, activation, and fetching. Service workers can be used for various purposes, such as offline functionality, push notifications, and background sync. Service workers can be debugged using the browser's developer tools, which provide various features and options to inspect and manipulate the service worker's behavior and state. Service workers are a powerful and versatile tool for web development, as they enable creating fast, reliable, and engaging web experiences.