Service Workers in Angular: The Complete Setup Guide with Best Practices and Troubleshooting Tips

Service Workers in Angular: The Complete Setup Guide with Best Practices and Troubleshooting Tips

Service Workers are proxy servers that greatly enhance the performance of applications by managing caching. In addition to performance optimization, Service Workers can transform your application into a Progressive Web Application (PWA), allowing users to access it even when offline. Furthermore, Service Workers also enable the sending of push notifications to the users.

In this comprehensive guide, I’ll walk you through the process of setting up a Service Worker in your Angular app. I’ll cover best practices and provide troubleshooting tips to ensure a seamless implementation. By the end of this article, you’ll have an Angular app with a highly optimized Service Worker, allowing users to access it even offline.

Creating Angular App

Let’s quickly create a new Angular app using Angular CLI. Open your terminal and run the following command:

ng new angular-service-worker-setup

The Angular CLI will create a template application inside the angular-service-worker-setup folder. You can run it by navigating to the application folder and executing the following command in your terminal:

npm start

The template app should now be live and accessible at http://localhost:4200.

Service Worker Basic Setup

Let us now upgrade our Angular app to a Progressive Web Application (PWA), by setting up a Service Worker. Angular CLI makes this process simple. Go to the application folder and run the following command in your terminal:

ng add @angular/pwa

This command will make a few changes to your application, including:

  1. Adding Service Worker registration logic to the app.module.ts file.
  2. Adding default application icons to the assets/icons folder.
  3. Updating the index.html file with a manifest.webmanifest link.
  4. Updating the angular.json file with Service Worker configuration.
  5. Adding the ngsw-config.json file which specifies the configuration of the Service Worker.

The ngsw-config.json file tells the Service Worker how to manage caching. By default, it contains two asset groups with different configurations:

{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/manifest.webmanifest",
          "/*.css",
          "/*.js"
        ]
      }
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
        ]
      }
    }
  ]
}

The first group has the installMode property set to prefetch, meaning the Service Worker will fetch all listed resources while caching the current version of the application.

The second group has the installMode property set to lazy, meaning the Service Worker will only cache resources when they are requested. This is useful for resources like images with different resolutions, as the Service Worker will only cache the correct assets for the screen size. The updateMode property is set to prefetch, meaning the Service Worker will immediately cache any updated resources.

This basic setup is enough for integrating Service Worker into your application. Note that Service Worker is only added to production builds by default, so to test it, you’ll need to compile your application in production mode using the following command:

ng build

Service Worker Advanced Setup

Before we proceed with adding more code, let’s take a moment to understand how Service Workers actually work.

During the production build, Service Workers generate a hash for each file based on its content, and create a file called ngsw.json, which contains the hashes of all the resources. The version of the application is determined by the content of the ngsw.json file. If any of the cached files change, the file’s hash in ngsw.json will also change, causing the Angular Service Worker to treat the current set of files as a new version.

There are four different registration strategies for Service Workers in Angular, which determine when they will be registered with the browser:

  1. registerWhenStable:<timeout>: Register as soon as the application stabilizes (no pending micro-tasks or macro-tasks), but no later than <timeout> milliseconds. If the app hasn’t stabilized after <timeout> milliseconds, the Service Worker will be registered anyway. If <timeout> is omitted, the Service Worker will only be registered once the application stabilizes.

  2. registerImmediately: Register immediately.

  3. registerWithDelay:<timeout>: Register with a delay of <timeout> milliseconds. For example, use registerWithDelay:5000 to register the Service Worker after 5 seconds. If <timeout> is omitted, it defaults to 0, which will register the Service Worker as soon as possible but still asynchronously, once all pending micro-tasks are completed.

  4. An Observable factory function: A function that returns an Observable. At runtime, the function will be used to obtain and subscribe to the Observable, and the Service Worker will be registered as soon as the first value is emitted.

Angular uses registerWhenStable:30000 as the default registration strategy, meaning it will first wait for the application to stabilize and then register the Service Worker, or register it after 30 seconds if the application doesn’t stabilize by then.

Let’s start by updating the default registration strategy to registerWhenStable:10000 in the app.module.ts file. By default, the application waits for 30 seconds for stabilization which is quite a lot of time. If your application takes more than 30 seconds to stabilize, it’s an indicator that you need to optimize your code.

import { NgModule, isDevMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { ServiceWorkerModule } from '@angular/service-worker';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ServiceWorkerModule.register('ngsw-worker.js', {
      enabled: !isDevMode(),
      // Register the ServiceWorker as soon as the application is stable
      // or after 10 seconds (whichever comes first).
      registrationStrategy: 'registerWhenStable:10000'
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Let’s now make sure that users currently using our application are aware of the new updates and will be able to see the latest version. We can achieve this by periodically checking for updates and automatically reloading the application with the new version. To implement this, we’ll update the app.component.ts file, which serves as the main file for our application.

import { Component } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  constructor(private swUpdate: SwUpdate) {
    this.checkForNewVersion();

    // Check for new version every minute
    setInterval(() => this.checkForNewVersion(), 60 * 1000);
  }

  checkForNewVersion = async () => {
    try {
      // Check if Service Worker is supported by the Browser
      if (this.swUpdate.isEnabled) {
        // Check if new version is available
        const isNewVersion = await this.swUpdate.checkForUpdate();
        if (isNewVersion) {
          // Check if new version is activated
          const isNewVersionActivated = await this.swUpdate.activateUpdate();

          // Reload the application with new version if new version is activated
          if (isNewVersionActivated) window.location.reload();
        }
      }
    } catch (error) {
      console.log(
        `Service Worker - Error when checking for new version of the application: `,
        error
      );
      window.location.reload();
    }
  };
}

The above code checks every minute if the Service Worker is supported by the browser and then verifies if a new version is available. If it is, the code will attempt to activate the update and, if successful, reload the application with the updated version.

With this, we have completed the advanced setup for the Service Worker. By automatically checking for updates every minute, both new and current users will have access to the latest version of the application when it becomes available.

Troubleshooting Tips

Let’s now focus on some common issues that can arise when implementing a Service Worker in Angular.

The most frequent problem is that the Service Worker won’t serve the latest version of the application, causing users to interact with an outdated version. This is usually caused by a hash mismatch, which occurs when the hash computed by the Service Worker differs from the hash present in the ngsw.json file. The hash mismatch issue can occur for several reasons:

  1. Post-processing after production build: If any post-processing is done on a file after the production build has generated the ngsw.json file, it can result in a hash mismatch when the Service Worker computes it. To avoid this issue, be sure to avoid any post-processing after the production build is generated, such as changing API keys in the index.html file.

  2. Middle layer of caching data: If a middle layer of caching data is present between the client and the server, such as Nginx, it can cause a hash mismatch with the Service Worker.

  3. Deploying the application with Git: When deploying the application with Git using a post-receive git hook, line endings can be replaced by default, causing the files on the server to differ from the original ones, resulting in a hash mismatch.

  4. Serving files through CDN: CDN can apply different optimizations on the files, such as minification or compression, which can change the hash value for these files and result in a hash mismatch for the Service Worker.

If you’re experiencing the issue where the application keeps loading an outdated version, it’s important to carefully check all of these potential causes and make modifications to the logic as needed to resolve the problem.

Additionally, it’s a good practice to regularly check for updates and to reload the application with the new version as soon as it becomes available. This ensures that both new and current users will see the latest version of the application when it’s available, enhancing the overall user experience.

Conclusion

Service Workers are proxy servers used in applications to improve performance by managing caching, making the application a Progressive Web Application (PWA) that can be accessed offline, and enabling push notifications. In this guide, we learned how to set up a Service Worker in an Angular application, including best practices and troubleshooting tips.

We first created an Angular application using Angular CLI, and then upgraded the application to a Progressive Web Application (PWA) by setting up a Service Worker. Angular CLI made this process simple by adding Service Worker registration logic, default application icons, and the ngsw-config.json file that tells the Service Worker how to manage caching.

We covered four different registration strategies for Service Workers in Angular that determine when they will be registered with the browser. Then we implemented advanced Service Worker logic that ensures that both new and current users will see the latest version of the application.

In the end, we covered common issues that can arise when implementing a Service Worker in Angular.

Resources

If you want to use this setup for your own projects, you can find the source code on my GitHub repository.