Progressive Web Apps (PWA) with Angular
A Progressive Web App (PWA) is a web application that uses modern web technologies to deliver app-like experiences. PWAs can be installed on devices, work offline, receive push notifications, and load instantly — just like native apps.
Angular provides first-class PWA support through the @angular/pwa package, which adds a
Service Worker, Web App Manifest, and sensible defaults with a single command.
PWA Core Features
Installable — users can add the app to their home screen
Offline support — works without internet via Service Worker caching
Fast loading — assets cached after first visit load instantly
Push notifications — re-engage users even when the browser is closed
Responsive — works on any device and screen size
Secure — served over HTTPS
Adding PWA to Your Angular Project
ng add @angular/pwa
CREATE ngsw-config.json CREATE src/manifest.webmanifest CREATE src/assets/icons/icon-72x72.png CREATE src/assets/icons/icon-96x96.png CREATE src/assets/icons/icon-128x128.png CREATE src/assets/icons/icon-144x144.png CREATE src/assets/icons/icon-152x152.png CREATE src/assets/icons/icon-192x192.png CREATE src/assets/icons/icon-384x384.png CREATE src/assets/icons/icon-512x512.png UPDATE angular.json UPDATE package.json UPDATE src/app/app.config.ts UPDATE src/index.html
This single command sets up everything needed for a basic PWA.
The Web App Manifest
The manifest file (src/manifest.webmanifest) controls how your app appears when installed:
{
"name": "My Angular PWA",
"short_name": "MyPWA",
"description": "An awesome Angular Progressive Web App",
"theme_color": "#1976d2",
"background_color": "#fafafa",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "assets/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"shortcuts": [
{
"name": "Dashboard",
"url": "/dashboard",
"icons": [{ "src": "assets/icons/icon-96x96.png", "sizes": "96x96" }]
}
]
}Manifest Field | Purpose |
|---|---|
display: standalone | Hides browser UI — looks like a native app |
display: minimal-ui | Shows minimal browser controls |
display: fullscreen | No browser chrome at all |
theme_color | Colors the browser toolbar on mobile |
background_color | Shown while app loads (splash screen) |
start_url | URL opened when app is launched |
scope | Restricts navigation that stays in PWA mode |
shortcuts | Quick actions from the app icon long-press menu |
Service Worker Configuration (ngsw-config.json)
The ngsw-config.json file controls what Angular's Service Worker caches and how:
{
"$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)"
]
}
}
],
"dataGroups": [
{
"name": "api-freshness",
"urls": ["/api/live-data"],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 100,
"maxAge": "3d",
"timeout": "10s"
}
},
{
"name": "api-performance",
"urls": ["/api/static-data"],
"cacheConfig": {
"strategy": "performance",
"maxSize": 100,
"maxAge": "1d"
}
}
],
"navigationUrls": [
"/**",
"!/**/*.*",
"!/**/*__*",
"!/**/*__*/**"
]
}Caching Strategies
Strategy | Behavior | Best For |
|---|---|---|
performance | Serve from cache, update in background | API data that can be slightly stale |
freshness | Try network first, fall back to cache on timeout | Real-time data with offline fallback |
prefetch | Cache immediately at SW install | App shell, critical CSS/JS |
lazy | Cache on first request | Images, non-critical assets |
Registering the Service Worker
The ng add @angular/pwa command automatically registers the Service Worker. Here's what it adds to your app config:
// app.config.ts
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideServiceWorker } from '@angular/service-worker';
export const appConfig: ApplicationConfig = {
providers: [
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(), // Only active in production
registrationStrategy: 'registerWhenStable:30000',
}),
],
};isDevMode() returns true in dev). Build with ng build and serve the dist folder to test PWA features.SwUpdate — Handling App Updates
The SwUpdate service lets you check for and apply app updates:
import { Component, inject } from '@angular/core';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { filter } from 'rxjs/operators';
@Component({
standalone: true,
selector: 'app-root',
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 = false;
constructor() {
if (this.swUpdate.isEnabled) {
// Listen for available updates
this.swUpdate.versionUpdates
.pipe(filter((event): event is VersionReadyEvent =>
event.type === 'VERSION_READY'
))
.subscribe(() => {
this.updateAvailable = true;
});
// Check for updates periodically (every 6 hours)
setInterval(() => {
this.swUpdate.checkForUpdate();
}, 6 * 60 * 60 * 1000);
}
}
async applyUpdate() {
await this.swUpdate.activateUpdate();
document.location.reload();
}
}SwPush — Push Notifications
import { Component, inject } from '@angular/core';
import { SwPush } from '@angular/service-worker';
import { HttpClient } from '@angular/common/http';
@Component({
standalone: true,
selector: 'app-notifications',
template: `<button (click)="subscribeToNotifications()">Enable Notifications</button>`,
})
export class NotificationsComponent {
private swPush = inject(SwPush);
private http = inject(HttpClient);
// Your VAPID public key from the push notification server
readonly VAPID_PUBLIC_KEY = 'YOUR_PUBLIC_VAPID_KEY';
async subscribeToNotifications() {
try {
const subscription = await this.swPush.requestSubscription({
serverPublicKey: this.VAPID_PUBLIC_KEY,
});
// Send subscription to your backend
this.http.post('/api/push-subscriptions', subscription).subscribe();
} catch (err) {
console.error('Could not subscribe to notifications', err);
}
}
constructor() {
// Handle incoming push notifications
this.swPush.messages.subscribe((message: any) => {
console.log('Push message received:', message);
});
// Handle notification click actions
this.swPush.notificationClicks.subscribe(({ action, notification }) => {
if (action === 'open') {
window.open(notification.data.url);
}
});
}
}Background Sync
Background sync lets your app retry failed requests when connectivity is restored. While Angular's built-in SW doesn't directly support it, you can use the native API:
// Custom service worker extension (ngsw-worker.js supplements)
// In a custom sw.js file that you merge with ngsw-worker.js
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncPendingMessages());
}
});
async function syncPendingMessages() {
const pending = await getPendingFromIndexedDB();
for (const item of pending) {
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(item),
});
await removeFromIndexedDB(item.id);
}
}Testing Your PWA
Build for production: ng build
Serve the build locally: npx http-server dist/my-app/browser
Open Chrome DevTools → Application tab → Service Workers
Check "Manifest" section for app install prompt
Use Lighthouse audit: DevTools → Lighthouse → check "Progressive Web App"
Test offline: DevTools → Network → check "Offline" → reload the page
# Build production bundle ng build # Serve with a static server (required for SW to work) npx http-server dist/my-app/browser -p 8080
Lighthouse PWA Checklist
Served over HTTPS
Registers a Service Worker
Has a valid web app manifest with name, icons, start_url, display
Icons are at least 192x192 and 512x512
Theme color set in manifest and index.html meta tag
App shell loads when offline
First Contentful Paint under 3 seconds on mobile
Page responds with 200 when offline
Deploying a PWA
PWAs must be deployed to HTTPS. The easiest options:
Firebase Hosting — free HTTPS, CDN, automatic headers for SPA routing
Vercel / Netlify — zero-config HTTPS deployment
GitHub Pages — free for public repos
AWS CloudFront + S3 — scalable CDN deployment
# Deploy to Firebase Hosting npm install -g firebase-tools firebase login firebase init hosting ng build firebase deploy