Skip to content
corpus.web

Progressive Web Apps (PWA)

Registering a service worker gives an Angular app offline support, cached repeat loads, background updates, and installability

Baseline Angular 22.1.1
Kind Concept
View source

Modern Angular only No equivalent exists in the original 100 Days series. Written fresh for Angular v22.

Lead

Lead with this: A Progressive Web App adds offline support, background updates, and home screen installability to your Angular app by registering a service worker — a background script that intercepts network requests and decides whether to serve them from cache or the network.

What it is

A Progressive Web App (PWA) is a web app that uses modern browser APIs to behave more like a native app:

CapabilityWhat enables it
Works offlineService worker caches app shell + assets
Fast repeat loadsAssets served from cache, not network
Background updatesNew version downloaded while user uses the old one
Installablemanifest.webmanifest + service worker → Add to Home Screen
Push notificationsPush API + service worker (advanced, not covered here)

Angular's @angular/service-worker package handles the hard parts: it generates a versioned asset manifest at build time, ships a pre-built service worker (ngsw-worker.js), and provides SwUpdate — an injectable service for detecting and applying updates.

How it works under the hood

Old model — plain web app, no offline support

A plain Angular app has no concept of offline:

User visits pageBrowser requests index.html from serverBrowser parses HTML, requests main.js, styles.css, assetsAll requests go to the network — if network is unavailable, app failsUser must reload when a new version is deployed to see updates

Every visit pays full network cost. A slow connection means a slow app. No network means a broken app. The user has no way to install the app to their home screen for quick re-access.

New model — service worker as network proxy

A service worker is a JavaScript file that runs in the background in a separate thread from the main page. Once registered, it intercepts every network request the page makes — JS bundles, CSS, images, API calls — and decides how to handle each one:

User visits page (first time)Browser registers ngsw-worker.js (the Angular service worker)SW downloads and caches all prefetch assets (app shell)Future visits: SW intercepts requests    ├─ App shell (JS, CSS, HTML): served from cache — near-instant    └─ API requests: served per the configured strategy         ├─ performance strategy: cache-first, background update         └─ freshness strategy: network-first, fall back to cache
User is offlineSW serves everything from cache — app still worksNetwork request to API fails: app shows cached data or graceful error
New version deployedOn next page load, SW fetches ngsw.json and compares hashesChanged hashes → SW downloads new bundles in backgroundSwUpdate.versionUpdates emits VersionReadyEventApp prompts user: "Update available — click to reload"User confirms → activateUpdate() + window.location.reload()

How Angular's service worker manages versions

At build time, Angular generates ngsw.json — a manifest that lists every cached file with its content hash. The service worker loads this manifest on install and on each application reload.

When you deploy a new version, some hashes change. The service worker detects the change, downloads the new files in the background (without disrupting the current user session), and flags itself as ready. The old version keeps serving the current user until they reload.

This version-atomic approach means users never see a mixed state of old and new files — they either get the complete old version or the complete new version, never a mix.

Setup

bash
ng add @angular/pwa

This command:

  • Installs @angular/service-worker
  • Creates src/manifest.webmanifest (app name, icons, colors)
  • Creates src/ngsw-config.json (caching rules)
  • Adds icon files to src/assets/icons/
  • Configures angular.json to bundle the service worker
  • Adds provideServiceWorker() to your providers
  • Adds <link rel="manifest"> and theme-color meta tag to index.html

Your app.config.ts gets:

typescript
// Generated by ng add @angular/pwaimport { provideServiceWorker } from '@angular/service-worker';import { isDevMode } from '@angular/core';
export const appConfig: ApplicationConfig = {  providers: [    provideRouter(routes),    provideServiceWorker('ngsw-worker.js', {      enabled: !isDevMode(),               // disabled during ng serve      registrationStrategy: 'registerWhenStable:30000'      // Register when the app is stable, or after 30 seconds — whichever comes first    }),  ],};
typescript
// NgModule approach (Angular 2–13)@NgModule({  imports: [    ServiceWorkerModule.register('ngsw-worker.js', {      enabled: !isDevMode(),      registrationStrategy: 'registerWhenStable:30000',    }),  ],})export class AppModule {}

Critical: Service workers only run in production builds. ng serve (development mode) explicitly disables them. Always test PWA features against a production build:

bash
ng build && npx http-server dist/my-app/browser -p 8080 -c-1

The -c-1 flag disables http-server's own caching so you see the service worker's caching, not the server's.

Configuring what to cache — ngsw-config.json

The generated ngsw-config.json has two sections: assetGroups (static app files) and dataGroups (API responses).

assetGroups — caching static app files

json
{  "assetGroups": [    {      "name": "app",      "installMode": "prefetch",      "updateMode": "prefetch",      "resources": {        "files": [          "/favicon.ico",          "/index.html",          "/manifest.webmanifest",          "/*.css",          "/*.js"        ]      }    },    {      "name": "assets",      "installMode": "lazy",      "updateMode": "lazy",      "resources": {        "files": [          "/assets/**",          "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"        ]      }    }  ]}
OptionValuesMeaning
installMode: 'prefetch'Download and cache at install time (app shell)
installMode: 'lazy'Cache only when first requested
updateMode: 'prefetch'Download new version proactively when app updates
updateMode: 'lazy'Only update in cache when next requested

Keep your critical app shell (index.html, JS bundles, CSS) on prefetch. Large image assets should be lazy — no point downloading images the user might never visit.

dataGroups — caching API responses

json
{  "dataGroups": [    {      "name": "api-performance",      "urls": ["/api/products/**", "/api/categories"],      "cacheConfig": {        "strategy": "performance",        "maxSize": 100,        "maxAge": "1h",        "timeout": "3s"      }    },    {      "name": "api-freshness",      "urls": ["/api/user/**", "/api/cart"],      "cacheConfig": {        "strategy": "freshness",        "maxSize": 20,        "maxAge": "5m",        "timeout": "3s"      }    }  ]}
StrategyBehaviorUse when
performanceCache-first. Serve from cache immediately, refresh in backgroundProduct listings, reference data — stale is acceptable
freshnessNetwork-first. Try network; fall back to cache if offlineUser data, cart, anything that must be current

timeout caps how long to wait for the network before falling back to cache. maxAge sets how long cached entries stay valid. maxSize limits how many URLs this group caches.

Handling app updates — SwUpdate

SwUpdate is an injectable service that exposes the service worker's update lifecycle as an Observable:

typescript
import { Component, inject, signal } from '@angular/core';import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';import { filter } from 'rxjs';import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({  selector: 'app-root',  standalone: true,  template: `    @if (updateAvailable()) {      <div class="update-banner">        A new version is available.        <button (click)="applyUpdate()">Update now</button>      </div>    }    <router-outlet />  `,})export class AppComponent {  private swUpdate = inject(SwUpdate);  updateAvailable = signal(false);
  constructor() {    if (!this.swUpdate.isEnabled) return;  // graceful degradation
    // versionUpdates is the modern API (versionUpdates replaced available/activated)    this.swUpdate.versionUpdates.pipe(      filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'),      takeUntilDestroyed()    ).subscribe(() => {      this.updateAvailable.set(true);    });
    // Handle the rare case where the service worker enters an unrecoverable state    this.swUpdate.unrecoverable.pipe(      takeUntilDestroyed()    ).subscribe(() => {      window.location.reload();    });  }
  async applyUpdate(): Promise<void> {    await this.swUpdate.activateUpdate();    window.location.reload();  }}

Proactive update checks

The service worker automatically checks for updates on each page reload. For long-running sessions (apps left open in a tab for hours), you may want to check periodically:

typescript
constructor() {  if (!this.swUpdate.isEnabled) return;
  // Check for updates every 6 hours in long-running sessions  interval(6 * 60 * 60 * 1000).pipe(    takeUntilDestroyed()  ).subscribe(() => {    this.swUpdate.checkForUpdate().catch(() => {      // Silently ignore — user may be offline    });  });}

Common mistakes

Mistake 1 — Testing with ng serve

Service workers are explicitly disabled when isDevMode() is true. If you test your PWA with ng serve, the service worker never registers:

bash
# ❌ Service worker disabled — PWA features don't workng serve
# ✅ Production build + static server — service worker activeng build --configuration=productionnpx http-server dist/my-app/browser -c-1

Mistake 2 — Using the deprecated SwUpdate.available Observable

swUpdate.available and swUpdate.activated were deprecated. Use the unified swUpdate.versionUpdates with type narrowing:

typescript
// ❌ Deprecated — shows warning in v22this.swUpdate.available.subscribe(() => this.updateAvailable = true);
// ✅ Current API — versionUpdates with type guardthis.swUpdate.versionUpdates.pipe(  filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY')).subscribe(() => this.updateAvailable.set(true));

Mistake 3 — Not checking swUpdate.isEnabled before calling methods

In browsers that don't support service workers, swUpdate.isEnabled is false. Calling checkForUpdate() or subscribing to versionUpdates on a disabled SwUpdate returns rejected Promises and never-emitting Observables — but calling methods directly can still throw:

typescript
// ❌ Throws if service workers unsupportedthis.swUpdate.checkForUpdate();
// ✅ Guard with isEnabled firstif (this.swUpdate.isEnabled) {  this.swUpdate.checkForUpdate();}

Mistake 4 — Caching user-specific API endpoints aggressively

Caching /api/user/profile with performance strategy means User A might see User B's cached data if they log in on the same device. Always use freshness strategy for user-specific, auth-protected, or mutable data:

json
{  "name": "user-data",  "urls": ["/api/user/**", "/api/account/**"],  "cacheConfig": {    "strategy": "freshness",   // ✅ network-first for user data    "maxAge": "1m",    "maxSize": 10  }}

How this evolved

  • Angular 5 (2017): @angular/service-worker introduced alongside the @angular/pwa schematic. First production-ready service worker for Angular apps, with ngsw-config.json and the asset group model.

  • Angular 6–9 (2018–2020): Stability improvements. Push notifications support added via SwPush. Data groups for API caching added.

  • Angular 12 (2021): SwUpdate.available and SwUpdate.activated deprecated in favor of SwUpdate.versionUpdates — a unified Observable that emits typed VersionEvent objects.

  • Angular 14 (2022): Standalone: provideServiceWorker() introduced as the standalone alternative to ServiceWorkerModule.register().

  • Angular 22 (now): @angular/service-worker is stable and unchanged. SwUpdate.versionUpdates is the standard update API. The main forward- looking note: Angular's built-in service worker is intentionally simple. For advanced use cases (background sync, fine-grained push notification logic, custom offline pages), consider Workbox as a more capable alternative that can coexist with Angular.

See also

0%