Progressive Web Apps (PWAs) with service workers have been around for a long time and if you’re trying to optimise how your web app gets loaded, this might just be the thing you need. I recently did a fairly standard implementation of it with Angular and thought I’d share my experience here in the hopes that it might show how easy it actually is.

Before I get into it, let’s talk about what a PWA is and what a Service Worker is.

What is a PWA?

Believe it or not, not everyone enjoys the idea of mobile app stores bloated with native versions of every other company’s website (I have probably six different coffee shop apps on my phone). A better solution comes in the form of PWAs.

A PWA is a type of web application that combines the best features of traditional websites and native mobile apps. PWAs are designed to work seamlessly across different devices and offer an app-like experience while being accessible through a web browser.

Key Features of a PWA

  1. Progressive – Works for all users, regardless of browser choice, because it is built with progressive enhancement.
  2. Responsive – Adapts to any screen size (mobile, tablet, desktop).
  3. Works Offline – Uses Service Workers to cache content, allowing the app to function even without an internet connection.
  4. App-like Experience – Feels and behaves like a native app, with smooth animations and interactions.
  5. Fast Loading – Uses caching and efficient resource management to load quickly, even on slow networks.
  6. Secure – Served over HTTPS to prevent data tampering.
  7. Push Notifications – Can send notifications like native apps, enhancing user engagement.
  8. Installable – Can be added to a home screen on mobile or desktop without going through an app store.
  9. Auto Updates – Updates automatically in the background without requiring user intervention.

Some notable examples of PWAs

  • Spotify Web Player
  • Pinterest
  • Uber
  • Microsoft Outlook
  • Instagram

It’s not a new concept, it’s been around for a long time with origins being traced back to the early 2000s, with the introduction of XMLHttpRequests - which allowed us to make server side calls without refreshing the page (no postbacks!).

The term Progressive Web App was officially coined somewhere in 2015 by Google though.

Where do Service Workers come in?

A service worker is a sophisticated JavaScript file that sits in the background and runs independently of a web page. Think of it as a proxy between the browser and the network.

Key Features of a Service Worker

  1. Runs in the Background – Works separately from the main webpage, so it doesn’t block rendering.
  2. Intercepts Network Requests – Can cache resources and serve them from cache when offline.
  3. Enables Offline Support – Allows users to access a web app even without an internet connection.
  4. Handles Push Notifications – Can receive and display push notifications, even when the app is closed.
  5. Background Sync – Can sync data in the background when the network is available again.
  6. Improves Performance – Reduces load times by serving cached assets instead of making network requests.
  7. Secure – Only works over HTTPS to prevent security risks.

How it works (in a nutshell)

  1. Installation – The service worker is registered and installed when a user visits the site.
  2. Activation – Once installed, it activates and starts controlling the webpage.
  3. Interception – It can intercept and modify network requests (e.g., serve cached data).
  4. Events – It listens for events like fetch (network requests), push (notifications), and sync (background sync).

Sounds simple enough? It’s actually even more simple to implement - here’s the simplest form I could come up with:

// Register the Service Worker in the main JavaScript file
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
    .then(() => console.log("Service Worker Registered"))
    .catch(err => console.log("Service Worker Registration Failed", err));
}

and then the service-worker.js itself:

// service-worker.js
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('my-cache').then(cache => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/script.js',
        '/images/logo.png'
      ]);
    })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});

Angular implementation

The angular implementation is also quite simple though that’s because I’m just scratching the surface with it. When dealing with aspects like the manifest and routing, things can get trickier, but the complexity depends on your app and how far you want to take it. For now let’s just get to the step-by-step of it:

Step 1 - Install @angular/pwa

Simply navigate to the root folder of your project and run

ng add @angular/pwa

This command will:

  • Install @angular/service-worker
  • Set "serviceWorker" : true in your angular.json file.
  • Generate ngsw-config.json config file for the service worker in the root.
Version discrepancies

Depending on your version of angular you might have to install a specific version of the package by appending it to @angular/pwa@12.1.0. Since Angular 12, the package has maintained version parity with Angular but prior to that (as in my case) there is a slight difference in the versioning:

Angular 11.2.11 would play best with PWA version 0.1102.11 - as an example.

Step 2 - Configure the caching.

ngsw-config.json looks like this:

"assetGroups": [
  {
    "name": "app",
    "installMode": "prefetch",
    "updateMode": "prefetch",
    "resources": {
      "files": [
        "/favicon.ico",
        "/index.html",
        "/*.css",
        "/*.js"
      ]
    }
  },
  {
    "name": "assets",
    "installMode": "lazy",
    "updateMode": "prefetch",
    "resources": {
      "files": [
        "/assets/**"
      ]
    }
  }
]

Most of the boilerplate config should suffice but you could tweak the installMode if you liked.

  • installMode: "prefetch" → Downloads assets when the app is first loaded.
  • installMode: "lazy" → Downloads assets when requested.
Data Group Caching (API Requests)

You might want to cache some API requests, simply add the following property to your ngsw-config.json

"dataGroups": [
  {
    "name": "api-calls",
    "urls": ["https://api.yourbackend.com/**"],
    "cacheConfig": {
      "strategy": "freshness",
      "maxSize": 50,
      "maxAge": "1h",
      "timeout": "10s"
    }
  }
]
  • "strategy": "freshness" → Tries to fetch from the network first and falls back to cache if offline.
  • "maxAge": "1h" → Keeps cached responses for 1 hour.

Step 4 - Deploy and test

Service workers only work in a production build so make sure to run:

ng build --prod

Then you can simply serve the build locally with:

npx http-server -p 8080 -c-1 dist/

Simple as. Navigate to the page and verify the worker is running:

  • Open DevTools (usually F12).
  • Go to Application > Service Workers
  • You should see your registered service worker there.

(You could also just stop the web server and refresh the page and all should work fine)

Step 5 - Handling Service Worker Updates

Generally your service worker will check to see if they’ve gone “stale” and refresh themselves if possible. But if you wanted to implement your own way of doing this (we’ve all seen apps asking us to ‘Click here to update’) it’s quite simple. You can use the SwUpdate service as follows:

constructor(private swUpdate: SwUpdate) {
if (this.swUpdate.isEnabled) {
    this.swUpdate.available.subscribe(() => {
        if (confirm('A new version is available. Load new version?')) {
            window.location.reload();
        }
    });
}
}

Pretty simple but obviously there’s room for much more intricate implementations.

Tips

If you’re having trouble clearing your cache to the following:

  1. Open DevTools.
  2. Go to Application > Clear storage.
  3. Click Clear site data.

You can use chrome://serviceworker-internals/ to see active service workers - this could help you debug yours.

Conclusion

This is a really easy way to get your app cached locally and help reduce load times (and requests to the server). It can also act as a great way to cache-bust as your service worker is a bit more aware of what’s changed.

All-in-all, it’s a really easy implementation and though there could be some pitfalls, if you test your app thoroughly you can avoid most of these and hopefully be on your way to building a fully-fledged Progressive Web App.