Progressive Web Apps — Service Workers, Caching, and Push Notifications

A Progressive Web App (PWA) provides a native app-like experience: installable to the home screen, works offline, receives push notifications, and updates silently in the background. Angular’s @angular/pwa package automates most of the PWA setup — generating a service worker, configuring caching strategies, and creating the Web App Manifest. For a MEAN Stack task manager, PWA capabilities mean users can access their task list offline and get instant loads on repeat visits.

PWA Capabilities

Feature Technology Angular Support
Offline functionality Service Worker cache @angular/service-worker + ngsw-config.json
Installable to home screen Web App Manifest Auto-generated by ng add @angular/pwa
Push notifications Push API + Service Worker SwPush service from @angular/service-worker
Silent updates Service Worker update lifecycle SwUpdate service

ngsw-config.json Caching Strategies

Strategy Behaviour Best For
prefetch Download and cache immediately on install App shell, critical JS/CSS bundles
lazy Cache on first request, serve from cache after Images, fonts, non-critical assets
freshness (API) Try network first, fall back to cache Dynamic API data — fresh when online
performance (API) Serve from cache, update in background Semi-static data — speed priority
Note: Service workers only work over HTTPS (or localhost for development). They do not intercept requests during development with ng serve — you need to build (ng build --configuration production) and serve the dist/ folder with a static server (npx serve dist/app/browser) to test service worker behaviour locally.
Tip: Implement an in-app update notification using SwUpdate.versionUpdates. When a new version is deployed and the service worker detects it, subscribe and show a toast: “A new version is available. Click to update.” When the user clicks, call swUpdate.activateUpdate() then document.location.reload() to apply the update.
Warning: Do NOT cache user-specific authenticated API responses with the performance strategy — User A’s tasks could be served to User B after switching accounts. Only cache truly public, non-user-specific data. For user data, use freshness strategy so the network is always tried first, with cache only as offline fallback.

Complete PWA Setup

# Add PWA support
ng add @angular/pwa
# Generates: ngsw-config.json, manifest.webmanifest, icons
# Updates: app.config.ts (adds provideServiceWorker), index.html

# Test PWA locally (ng serve does NOT activate service worker)
ng build --configuration production
npx serve dist/app/browser -p 4200
// ngsw-config.json
{
    "$schema": "./node_modules/@angular/service-worker/config/schema.json",
    "index": "/index.html",
    "assetGroups": [
        {
            "name": "app-shell",
            "installMode": "prefetch",
            "updateMode": "prefetch",
            "resources": {
                "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"]
            }
        },
        {
            "name": "assets",
            "installMode": "lazy",
            "updateMode": "lazy",
            "resources": {
                "files": ["/assets/**", "/*.(svg|jpg|jpeg|png|webp|woff|woff2)"]
            }
        }
    ],
    "dataGroups": [
        {
            "name": "api-freshness",
            "urls": ["/api/v1/tasks/**"],
            "cacheConfig": { "strategy": "freshness", "maxSize": 50, "maxAge": "1h", "timeout": "10s" }
        },
        {
            "name": "api-public",
            "urls": ["/api/v1/config"],
            "cacheConfig": { "strategy": "performance", "maxSize": 5, "maxAge": "24h" }
        }
    ]
}
// app.config.ts
import { provideServiceWorker } from '@angular/service-worker';

export const appConfig = {
    providers: [
        provideServiceWorker('ngsw-worker.js', {
            enabled: environment.production,
            registrationStrategy: 'registerWhenStable:30000',
        }),
    ],
};

// Update service
@Injectable({ providedIn: 'root' })
export class AppUpdateService {
    private swUpdate = inject(SwUpdate);
    private toast    = inject(ToastService);

    constructor() {
        if (!this.swUpdate.isEnabled) return;

        // Check every 6 hours
        interval(6 * 60 * 60 * 1000).subscribe(() => this.swUpdate.checkForUpdate());

        // Prompt user when update ready
        this.swUpdate.versionUpdates.pipe(
            filter((e): e is VersionReadyEvent => e.type === 'VERSION_READY'),
        ).subscribe(() => {
            this.toast.info('New version available!', {
                action: 'Update',
                onAction: () => this.applyUpdate(),
            });
        });

        // Handle corrupted cache
        this.swUpdate.unrecoverable.subscribe(() => {
            alert('App needs to reload. Reloading now.');
            document.location.reload();
        });
    }

    async applyUpdate(): Promise<void> {
        await this.swUpdate.activateUpdate();
        document.location.reload();
    }
}

How It Works

Step 1 — The Service Worker Is a Background Script

A service worker runs in a separate thread with no DOM access but can intercept every network request. Angular’s generated ngsw-worker.js reads the cache manifest, caches specified assets, and intercepts fetch requests to serve cached responses when offline or to improve performance.

Step 2 — Install Phase Prefetches Critical Assets

When the service worker first installs, it fetches and caches all resources with installMode: 'prefetch' — the app shell, JavaScript bundles, CSS. These are downloaded in one batch so the app works offline from the very first visit. lazy assets are cached only when first requested.

Step 3 — Freshness Strategy Keeps API Data Current

The freshness strategy always attempts the network first and falls back to cache only if the request times out or fails. This ensures users see current task data when online, while still showing cached data offline. The timeout: '10s' option prevents infinite loading spinners.

Step 4 — SwUpdate Manages the Update Lifecycle

When the service worker detects a new version (changed ngsw.json hash), it downloads it in the background. The old version keeps running. swUpdate.versionUpdates emits VERSION_READY. The app shows a notification; activateUpdate() + location.reload() switches to the new version.

Step 5 — The Web App Manifest Enables Installation

The manifest.webmanifest tells the browser the app’s name, icons, theme colour, and entry URL. When a user visits over HTTPS with an active service worker and manifest, the browser offers an “Add to home screen” prompt. After installation, the app launches without browser chrome in standalone display mode.

Common Mistakes

Mistake 1 — Testing service worker with ng serve

❌ Wrong — service workers are disabled in ng serve:

ng serve   # service worker NOT active

✅ Correct — build and serve production bundle:

ng build --configuration production
npx serve dist/app/browser -p 4200

Mistake 2 — Caching authenticated user data with performance strategy

❌ Wrong — User A’s tasks served to User B:

{ "urls": ["/api/**"], "cacheConfig": { "strategy": "performance" } }

✅ Correct — use freshness for user-specific data, or don’t cache it:

{ "urls": ["/api/v1/tasks/**"], "cacheConfig": { "strategy": "freshness", "timeout": "10s" } }

Mistake 3 — Not handling the unrecoverable state

❌ Wrong — corrupted cache leaves app broken:

// No unrecoverable handler — app freezes

✅ Correct:

this.swUpdate.unrecoverable.subscribe(() => {
    document.location.reload();
});

Quick Reference

Task Code / Command
Add PWA ng add @angular/pwa
Register SW provideServiceWorker('ngsw-worker.js', { enabled: isProd })
Handle new version swUpdate.versionUpdates.pipe(filter(e => e.type === 'VERSION_READY'))
Apply update await swUpdate.activateUpdate(); location.reload()
Push subscription swPush.requestSubscription({ serverPublicKey: VAPID_KEY })
Cache app shell assetGroups with installMode: 'prefetch'
Cache API safely dataGroups with strategy: 'freshness'
Test PWA locally ng build && npx serve dist/app/browser

🧠 Test Yourself

A new version of the Angular PWA is deployed. When does the user see it, and what triggers the transition?