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 |
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.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.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 |