| |

Angular 93 🅰️ Progressive Web Apps with Angular

⏱️ Estimated read: 15 min | |

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

CommandPurpose
ng add @angular/pwaAdd PWA support to the project

The Configuration Files

FilePurpose
ngsw-config.jsonService worker caching configuration
manifest.webmanifestInstalled app appearance
ngsw.jsonGenerated service worker manifest

The Caching Strategies

StrategyBehavior
performanceCache first, update in background
freshnessNetwork first, fallback to cache
optimizeCache for offline, not critical

The SwUpdate Events

EventWhen Emitted
VERSION_DETECTEDNew version found on server
VERSION_READYNew version downloaded and ready
VERSION_INSTALLATION_FAILEDInstallation failed
NO_NEW_VERSION_DETECTEDChecked, no new version
VERSION_FAILEDCritical failure affecting all clients

The Registration Strategies

StrategyBehavior
registerWhenStable:30000Wait for app to stabilize, max 30s
registerImmediatelyRegister immediately
registerWithDelay:5000Register after 5 seconds

The Browser Support

BrowserService 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

PitfallWhy It HappensFix
Service worker not registeringNot served over HTTPSUse HTTPS or localhost
Old content servedService worker cacheRefresh or unregister
Update not detectedNo pollingUse checkForUpdate()
Stale API dataAggressive cachingConfigure dataGroups carefully
Install prompt missingMissing manifest or iconsAdd icons and manifest
App broken after updateVersion mismatchReload 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

ItemValue
PWA definitionWeb app with native-like capabilities
Setup commandng add @angular/pwa
Service workerNetwork proxy with caching
Configurationngsw-config.json
Manifestmanifest.webmanifest
Update serviceSwUpdate
Update eventsVERSION_DETECTED, VERSION_READY, VERSION_INSTALLATION_FAILED
Check methodcheckForUpdate()
Cache strategiesperformance, freshness, optimize
HTTPS requirementRequired 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/pwa schematic configures the application for PWA support. It adds the @angular/service-worker package, creates ngsw-config.json, adds the manifest.webmanifest, installs icons, and registers the service worker .
  • The ngsw-config.json file defines the caching behavior. The assetGroups section caches application resources. The dataGroups section caches API responses with configurable strategies .
  • The SwUpdate service provides access to update events. The versionUpdates observable 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 isStable before starting the polling interval .
  • The manifest.webmanifest defines 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.

Was this helpful?


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!