AngularJSProgressive Web Apps

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

Bash
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:

JSON
{
  "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:

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-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": [
    "/**",
    "!/**/*.*",
    "!/**/*__*",
    "!/**/*__*/**"
  ]
}
Note
installMode: prefetch downloads resources eagerly during SW installation. installMode: lazy downloads on first request. Use prefetch for critical app shell, lazy for images and large assets.
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:

TS
// 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',
    }),
  ],
};
Tip
The Service Worker is disabled in development mode (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:

TS
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

TS
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:

TS
// 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
  1. Build for production: ng build

  2. Serve the build locally: npx http-server dist/my-app/browser

  3. Open Chrome DevTools → Application tab → Service Workers

  4. Check "Manifest" section for app install prompt

  5. Use Lighthouse audit: DevTools → Lighthouse → check "Progressive Web App"

  6. Test offline: DevTools → Network → check "Offline" → reload the page

Bash
# Build production bundle
ng build

# Serve with a static server (required for SW to work)
npx http-server dist/my-app/browser -p 8080
Warning
Service Workers only work over HTTPS (or localhost). Never test PWA features over a plain HTTP connection on a non-localhost domain — the SW registration will be silently skipped.
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

Bash
# Deploy to Firebase Hosting
npm install -g firebase-tools
firebase login
firebase init hosting
ng build
firebase deploy