Angular 93 🅰️ Progressive Web Apps with Angular
A Progressive Web App is a web application that uses modern browser capabilities to deliver an experience that feels like a native app. It can work offline, it can be installed on the home screen, it can send push notifications, and it can update itself in the background. Angular provides first-class support for PWAs through its service worker implementation, which is built into the framework and configured with a single CLI command.
The LFCA and Angular chapters in this series have covered infrastructure, components, and architecture. This chapter covers the layer that makes an Angular application installable and resilient: the service worker. The service worker is a script that runs in the browser, separate from the application, and intercepts network requests. It can serve cached responses when the network is unavailable, and it can update the application in the background without disrupting the user .
Key point: The Angular service worker is a network proxy. It intercepts every outgoing HTTP request from the application and decides how to respond. It can query a local cache and return a cached response, or it can forward the request to the network. The service worker persists after the user closes the tab. The next time the browser loads the application, the service worker loads first and can satisfy the entire application load from its cache .
Why Progressive Web Apps matter
A traditional web application depends on the network. If the network is slow or unavailable, the application is slow or unavailable. A PWA changes that contract. The application is cached, and the cache is served first.
The offline problem. A user opens the application on a train with no connectivity. A traditional web application shows an error page. A PWA loads from the service worker’s cache and works as if the network were present. The Angular service worker caches the application shell, the JavaScript bundles, the CSS, and the assets. When the user refreshes, the application loads from the cache .
The installation problem. A user visits the application repeatedly. With a PWA, the browser offers to install the application on the home screen or the desktop. The installed application runs in a standalone window without the browser chrome. It looks and feels like a native app. The manifest.webmanifest file defines the application’s name, icons, theme colors, and display mode .
The update problem. A new version of the application is deployed. The user should see the new version, but not in the middle of a task. The Angular service worker downloads the new version in the background and notifies the application when it is ready. The application can prompt the user to reload, or it can activate the update automatically .
The performance problem. Even on a fast network, round-trip delays add latency. A service worker that serves from the cache eliminates those delays. The application loads faster because the network is not in the critical path .
The trade-off. The service worker adds complexity. Caching is aggressive. A stale cache can serve old content. The update mechanism must be understood and configured. The Angular service worker is designed to minimize these risks, but the developer must still test offline behavior and update flows thoroughly .
a. Adding PWA Support
The Angular CLI provides a schematic that configures the application for PWA support. The command is:
ng add @angular/pwa
The schematic performs a series of actions. It adds the @angular/service-worker package to the project. It enables service worker build support in the CLI. It imports and registers the service worker with the application’s root providers. It updates the index.html file to include a link to the manifest.webmanifest and a meta tag for the theme color. It installs icon files to support the installed PWA. And it creates the service worker configuration file called ngsw-config.json .
The ngsw-config.json file specifies the caching behaviors. The default configuration caches the application shell, the build artifacts (JavaScript and CSS bundles), anything under the assets directory, and images and fonts directly under the output path . The configuration can be extended to cache API responses through the dataGroups section.
The manifest.webmanifest file defines the application’s appearance when installed. It includes the name, short name, theme color, background color, display mode, and icons in multiple sizes . The icons are required for the install prompt to appear on different platforms.
After the schematic runs, the project is built with ng build. The service worker is generated as part of the build. The application is now a PWA.
b. Caching and Offline Support
The service worker’s caching behavior is defined in ngsw-config.json. The file has two main sections: assetGroups and dataGroups.
assetGroups define the application resources that are cached. The default configuration includes an app group and an assets group. The app group caches the application shell, the index.html, and the build artifacts. The assets group caches the files under the assets directory and the images and fonts in the output path .
dataGroups define the runtime caching for API responses and other dynamic data. Each group specifies a URL pattern, a caching strategy (performance, freshness, or optimize), and a maximum size and age for the cache . The performance strategy serves from the cache first and updates in the background. The freshness strategy tries the network first and falls back to the cache. The optimize strategy is used for resources that should be stored in the cache but are not critical for the initial load.
The offline behavior can be tested by building the application, serving it with a local server, and then using the browser’s developer tools to simulate a network outage. In Chrome, the Network tab has an “Offline” option in the Throttling dropdown. With the service worker active, the application loads normally even when the network is offline .
The ngsw-config.json file is generated with a limited list of cacheable font and image extensions. If the application uses additional file types, the glob patterns must be modified. If the resourcesOutputPath or assets paths are changed after the configuration file is generated, the paths in ngsw-config.json must be updated manually .
c. Updates and the SwUpdate Service
The Angular service worker is designed to update the application in the background without disrupting the running version. When a new version is deployed, the service worker detects it, downloads it, and caches it. The running application continues to use the old version until the user refreshes or the application activates the update .
The SwUpdate service provides access to the update events. It is injected into a component or service and its versionUpdates observable emits events when a new version is detected, when it is ready, and when the installation fails .
import { SwUpdate } from '@angular/service-worker';
@Component({ selector: 'app-root', template: '...' })
export class AppComponent {
private swUpdate = inject(SwUpdate);
constructor() {
if (this.swUpdate.isEnabled) {
this.swUpdate.versionUpdates.subscribe((event) => {
if (event.type === 'VERSION_READY') {
if (confirm('New version available. Load it?')) {
window.location.reload();
}
}
});
}
}
}
The VERSION_READY event is emitted when a new version has been downloaded and is ready for activation. The application can prompt the user to reload, or it can call activateUpdate() to activate the new version without reloading. The Angular documentation warns that activating an update without reloading can cause a version mismatch between the application shell and lazy-loaded chunks. Reloading the page is the safest approach .
The checkForUpdate() method asks the service worker to check the server for new updates. The service worker checks for updates during initialization and on each navigation request. An application that changes frequently can poll for updates on a schedule. The polling should start after the application stabilizes to avoid delaying the service worker registration .
The unrecoverable observable emits an event when the service worker’s version is in a broken state that cannot be recovered without a full page reload. This can happen when the application’s cached assets are missing from the server and the service worker cannot serve a consistent version. The application should handle this event by reloading the page .
Complete Example Session
This session adds PWA support to an Angular application, configures caching, and handles updates.
// ============================================
// PART 1: ADD PWA SUPPORT
// ============================================
// Terminal
ng add @angular/pwa
// The schematic adds:
// - @angular/service-worker package
// - ngsw-config.json
// - manifest.webmanifest
// - Icon files
// - Updates to index.html
// ============================================
// PART 2: THE NGWS-CONFIG.JSON
// ============================================
// ngsw-config.json
{
"$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-performance",
"urls": ["/api/**"],
"cacheConfig": {
"strategy": "performance",
"maxSize": 100,
"maxAge": "1d"
}
}
]
}
// The app group caches the application shell.
// The assets group caches images and fonts.
// The api-performance group caches API responses.
// ============================================
// PART 3: THE UPDATE SERVICE
// ============================================
// update.service.ts
import { Injectable, inject } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable({ providedIn: 'root' })
export class UpdateService {
private swUpdate = inject(SwUpdate);
private snackBar = inject(MatSnackBar);
constructor() {
if (this.swUpdate.isEnabled) {
this.swUpdate.versionUpdates.subscribe((event) => {
switch (event.type) {
case 'VERSION_DETECTED':
console.log('Downloading new version...');
break;
case 'VERSION_READY':
this.promptUpdate();
break;
case 'VERSION_INSTALLATION_FAILED':
console.error('Update failed:', event.error);
break;
}
});
this.swUpdate.unrecoverable.subscribe(() => {
this.snackBar.open('Application is out of date. Reloading...', 'Reload')
.onAction().subscribe(() => window.location.reload());
});
}
}
private promptUpdate(): void {
const snackBarRef = this.snackBar.open(
'A new version is available.',
'Reload',
{ duration: 10000 }
);
snackBarRef.onAction().subscribe(() => {
window.location.reload();
});
}
}
// The service subscribes to version updates.
// When a new version is ready, it prompts the user to reload.
// ============================================
// PART 4: THE CHECK FOR UPDATES SERVICE
// ============================================
// check-for-update.service.ts
import { ApplicationRef, inject, Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { concat, interval } from 'rxjs';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CheckForUpdateService {
private appRef = inject(ApplicationRef);
private swUpdate = inject(SwUpdate);
constructor() {
const appIsStable$ = this.appRef.isStable.pipe(
first((isStable) => isStable === true)
);
const everySixHours$ = interval(6 * 60 * 60 * 1000);
const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
everySixHoursOnceAppIsStable$.subscribe(async () => {
try {
const updateFound = await this.swUpdate.checkForUpdate();
console.log(updateFound ? 'Update available.' : 'Already up to date.');
} catch (err) {
console.error('Failed to check for updates:', err);
}
});
}
}
// The service waits for the app to stabilize, then polls every six hours.
// ============================================
// PART 5: THE MANIFEST
// ============================================
// manifest.webmanifest
{
"name": "My Angular App",
"short_name": "MyApp",
"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"
}
]
}
// The manifest defines the installed app's appearance.
// ============================================
// PART 6: THE BUILD
// ============================================
// Terminal
ng build
// The build generates:
// - dist/app/browser/
// - index.html
// - main.js
// - ngsw.json (the service worker manifest)
// - ngsw-worker.js (the service worker)
// - manifest.webmanifest
// ============================================
// PART 7: TESTING OFFLINE
// ============================================
// Serve the build with a local server
npx http-server -p 8080 -c-1 dist/app/browser
// Open http://localhost:8080
// In Chrome DevTools, go to Network
// Set Throttling to Offline
// Refresh the page
// The application loads from the service worker cache
// ============================================
// PART 8: THE DEBUG PAGE
// ============================================
// Navigate to http://localhost:8080/ngsw/state
// The debug page shows:
// - Driver state
// - Latest manifest hash
// - Last update check
// - Clients
// - Idle task queue
// - Debug log
// ============================================
// PART 9: THE REGISTRATION OPTIONS
// ============================================
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideServiceWorker } from '@angular/service-worker';
export const appConfig: ApplicationConfig = {
providers: [
provideServiceWorker('ngsw-worker.js', {
enabled: true,
registrationStrategy: 'registerWhenStable:30000',
}),
],
};
// The registration strategy determines when the service worker is registered.
// The default waits for the app to stabilize, up to 30 seconds.
// ============================================
// PART 10: THE PWA CHECKLIST
// ============================================
// 1. Run ng add @angular/pwa
// 2. Customize manifest.webmanifest
// 3. Generate icons in all required sizes
// 4. Configure ngsw-config.json for caching
// 5. Handle update notifications with SwUpdate
// 6. Test offline behavior
// 7. Test installation on mobile and desktop
// 8. Validate with Lighthouse PWA audit
// 9. Deploy over HTTPS (required for service workers)
// 10. Monitor updates in production
The ten parts cover adding PWA support, the ngsw-config.json, the update service, the check-for-updates service, the manifest, the build, testing offline, the debug page, the registration options, and the PWA checklist.
Quick Reference
The CLI Command
| Command | Purpose |
|---|---|
ng add @angular/pwa | Add PWA support to the project |
The Configuration Files
| File | Purpose |
|---|---|
ngsw-config.json | Service worker caching configuration |
manifest.webmanifest | Installed app appearance |
ngsw.json | Generated service worker manifest |
The Caching Strategies
| Strategy | Behavior |
|---|---|
performance | Cache first, update in background |
freshness | Network first, fallback to cache |
optimize | Cache for offline, not critical |
The SwUpdate Events
| Event | When Emitted |
|---|---|
VERSION_DETECTED | New version found on server |
VERSION_READY | New version downloaded and ready |
VERSION_INSTALLATION_FAILED | Installation failed |
NO_NEW_VERSION_DETECTED | Checked, no new version |
VERSION_FAILED | Critical failure affecting all clients |
The Registration Strategies
| Strategy | Behavior |
|---|---|
registerWhenStable:30000 | Wait for app to stabilize, max 30s |
registerImmediately | Register immediately |
registerWithDelay:5000 | Register after 5 seconds |
The Browser Support
| Browser | Service Worker Support |
|---|---|
| Chrome | ✅ |
| Firefox | ✅ |
| Edge | ✅ |
| Safari | ✅ |
| Opera | ✅ |
| IE | ❌ |
| Opera Mini | ❌ |
Best Practices
✅ Do This:
# Use the CLI schematic
ng add @angular/pwa # ✅
// Check if the service worker is enabled
if (this.swUpdate.isEnabled) { } // ✅
// Prompt the user before reloading
if (confirm('New version available. Load it?')) { // ✅
// Wait for app stability before polling
concat(appIsStable$, everySixHours$) // ✅
// Configure data groups for API caching
"dataGroups": [{ "name": "api", "urls": ["/api/**"] }] // ✅
❌ Don’t Do This:
// Don't activate updates without reloading
this.swUpdate.activateUpdate(); // may break the app // ❌
// Don't poll for updates before the app stabilizes
interval(60000).subscribe(() => this.swUpdate.checkForUpdate()); // ❌
// Don't cache sensitive API responses
"urls": ["/api/auth/**"] // may cache tokens // ❌
// Don't skip HTTPS in production
// Service workers only register over HTTPS or localhost. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Service worker not registering | Not served over HTTPS | Use HTTPS or localhost |
| Old content served | Service worker cache | Refresh or unregister |
| Update not detected | No polling | Use checkForUpdate() |
| Stale API data | Aggressive caching | Configure dataGroups carefully |
| Install prompt missing | Missing manifest or icons | Add icons and manifest |
| App broken after update | Version mismatch | Reload the page |
Real-World Examples
1. Add PWA Support
ng add @angular/pwa
2. Check for Updates
const updateFound = await this.swUpdate.checkForUpdate();
3. Handle Version Ready
this.swUpdate.versionUpdates.subscribe((event) => {
if (event.type === 'VERSION_READY') { /* prompt */ }
});
4. Reload on Update
window.location.reload();
5. Handle Unrecoverable
this.swUpdate.unrecoverable.subscribe(() => window.location.reload());
6. Configure Caching
{ "assetGroups": [{ "name": "app", "installMode": "prefetch" }] }
7. Cache API Responses
{ "dataGroups": [{ "name": "api", "urls": ["/api/**"] }] }
8. Test Offline
npx http-server -p 8080 -c-1 dist/app/browser
9. Debug State
# Navigate to /ngsw/state
10. Registration Strategy
provideServiceWorker('ngsw-worker.js', { registrationStrategy: 'registerWhenStable:30000' })
Visual
The Service Worker as Network Proxy
┌──────────────────────────────────────────────┐
│ SERVICE WORKER AS NETWORK PROXY │
│ │
│ Application │
│ └─ HTTP request │
│ │ │
│ ▼ │
│ Service Worker │
│ ├─ Check cache │
│ │ ├─ Cache hit → return cached │
│ │ └─ Cache miss → forward to network │
│ └─ Update cache in background │
│ │
│ The service worker intercepts every request.│
│ It decides how to respond. │
│ │
└──────────────────────────────────────────────┘
The Update Flow
┌──────────────────────────────────────────────┐
│ UPDATE FLOW │
│ │
│ New version deployed │
│ │ │
│ ▼ │
│ Service worker detects new ngsw.json │
│ │ │
│ ▼ │
│ VERSION_DETECTED │
│ │ │
│ ▼ │
│ Download new assets in background │
│ │ │
│ ▼ │
│ VERSION_READY │
│ │ │
│ ├─ Prompt user to reload │
│ └─ Reload activates new version │
│ │
│ The old version continues to serve until │
│ the user reloads. │
│ │
└──────────────────────────────────────────────┘
The Caching Groups
┌──────────────────────────────────────────────┐
│ ngsw-config.json │
│ │
│ assetGroups: │
│ app: │
│ ├─ index.html │
│ ├─ *.css, *.js │
│ └─ prefetched on install │
│ │
│ assets: │
│ ├─ /assets/** │
│ ├─ images, fonts │
│ └─ lazy loaded, prefetched on update │
│ │
│ dataGroups: │
│ api-performance: │
│ ├─ /api/** │
│ ├─ cache first │
│ └─ max 100 entries, 1 day │
│ │
└──────────────────────────────────────────────┘
The Install Prompt
┌──────────────────────────────────────────────┐
│ PWA INSTALLATION │
│ │
│ Browser checks: │
│ ├─ Served over HTTPS │
│ ├─ Has manifest.webmanifest │
│ ├─ Has service worker │
│ └─ Has icons │
│ │
│ If all pass: │
│ └─ Browser offers install prompt │
│ │
│ Installed app: │
│ ├─ Runs in standalone window │
│ ├─ Has home screen icon │
│ └─ Works offline │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| PWA definition | Web app with native-like capabilities |
| Setup command | ng add @angular/pwa |
| Service worker | Network proxy with caching |
| Configuration | ngsw-config.json |
| Manifest | manifest.webmanifest |
| Update service | SwUpdate |
| Update events | VERSION_DETECTED, VERSION_READY, VERSION_INSTALLATION_FAILED |
| Check method | checkForUpdate() |
| Cache strategies | performance, freshness, optimize |
| HTTPS requirement | Required for service workers |
Key takeaways:
- The Angular service worker is a network proxy that intercepts HTTP requests. It can serve cached responses when the network is unavailable. It persists after the tab closes. The next time the application loads, the service worker loads first and can satisfy the entire load from its cache .
- The
ng add @angular/pwaschematic configures the application for PWA support. It adds the@angular/service-workerpackage, createsngsw-config.json, adds themanifest.webmanifest, installs icons, and registers the service worker . - The
ngsw-config.jsonfile defines the caching behavior. TheassetGroupssection caches application resources. ThedataGroupssection caches API responses with configurable strategies . - The
SwUpdateservice provides access to update events. TheversionUpdatesobservable emits events when a new version is detected, ready, or fails. The application can prompt the user to reload or activate the update . - Activating an update without reloading can break the application. The Angular documentation warns that it can cause a version mismatch between the application shell and lazy-loaded chunks. Reloading the page is the safest approach .
- Polling for updates must wait for the application to stabilize. The Angular service worker waits up to 30 seconds for the application to stabilize before registering. Constant polling prevents stabilization. The recommended pattern waits for
isStablebefore starting the polling interval . - The
manifest.webmanifestdefines the installed application’s appearance. It includes the name, icons, theme colors, and display mode. The icons are required for the install prompt to appear .
Remember: A Progressive Web App is a web application that uses the service worker to deliver a native-like experience. Angular provides the service worker implementation and the CLI schematic that configures it. The service worker caches the application, serves it offline, and updates it in the background. The SwUpdate service handles the update flow. The ngsw-config.json file defines the caching behavior. The manifest.webmanifest file defines the installed appearance. The PWA works over HTTPS, it is installable, and it is resilient to network failures.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!