Skip to main content

8 - Angular + PWA: Building an Installable App

Transform your Angular app into a PWA: service worker, manifest, caching strategies, offline support, push notifications, and install prompt.

What do you think about this article?

At a Glance

Transform your Angular app into a PWA: service worker, manifest, caching strategies, offline support, push notifications, and install prompt.

Angular + PWA: Building an Installable App

Progressive Web Apps (PWAs) bridge the gap between web applications and native apps. Through technologies like Service Workers, the Web App Manifest, and Push Notifications, a PWA delivers experiences comparable to a store-installed app, without the cost and complexity of native distribution. Angular integrates PWA support natively, making the transformation of a web application into an installable app a straightforward and well-documented process.

In this eighth article of the Modern Angular series, we will explore every aspect of building a PWA with Angular: from initial setup to advanced caching management, from offline support to push notifications, through automatic update mechanisms and custom install prompts. We will see how to test and debug every feature using Chrome DevTools.

What You Will Learn

  • What PWAs are and their advantages over native apps
  • How to set up Angular PWA with ng add @angular/pwa
  • Customizing the Web App Manifest for icons, colors, and display modes
  • Configuring the Service Worker with ngsw-config.json
  • Caching strategies: prefetch, lazy, network-first, cache-first
  • Offline support with fallback pages and IndexedDB
  • Push notifications with the SwPush service
  • Managing updates with SwUpdate
  • Custom install prompt with beforeinstallprompt
  • Testing and debugging with Chrome DevTools and Lighthouse

Modern Angular Series

# Article Topic
1Angular SignalsFine-grained reactivity
2Zoneless Change DetectionRemoving Zone.js
3New Template Syntax: @if, @for, @deferModern control flow
4Standalone ComponentsModule-free architecture
5Signal FormsReactive forms with Signals
6SSR and Incremental HydrationServer-Side Rendering
7Core Web Vitals in AngularPerformance optimization
8You are here - Angular PWAProgressive Web Apps
9Advanced Dependency InjectionDI patterns and tokens
10Migrating from Angular 17 to 21Step-by-step migration

1. What Are Progressive Web Apps

A Progressive Web App is a web application that uses modern technologies to deliver a user experience comparable to that of a native app. The term "progressive" means the app works for every user, regardless of browser choice, progressively enhancing the experience on browsers that support advanced features.

Key PWA Characteristics

PWAs distinguish themselves from traditional web applications through a set of capabilities that bring them closer to native apps distributed through app stores.

PWA vs Native App vs Traditional Web App

Feature Web App PWA Native App
Installable No Yes Yes
Works offline No Yes Yes
Push notifications No Yes Yes
Automatic updates Yes Yes Via store
Hardware access Limited Good Full
Distribution URL URL + Install App Store
Development cost Low Low High
SEO Yes Yes No (store indexing)

When to Choose a PWA

PWAs are the ideal choice when you want to reach the largest possible audience with a single codebase. They are perfect for e-commerce, blogs, business dashboards, productivity tools, and any application that benefits from offline access and push notifications. If your app requires deep hardware access (advanced Bluetooth, NFC, specific sensors), a native app may still be necessary.

2. Angular PWA Setup

Angular provides an official schematic that automates PWA configuration. With a single command, the CLI generates all the necessary files: the manifest, service worker configuration, and default icons.

Adding PWA support to an Angular project

# Add PWA to the project
ng add @angular/pwa

# What gets generated:
# - src/manifest.webmanifest    (Web App Manifest)
# - ngsw-config.json            (Service Worker configuration)
# - src/assets/icons/           (PWA icons in various sizes)
# - Changes to angular.json     (serviceWorker: true)
# - Changes to index.html       (link to manifest, meta theme-color)
# - Changes to app.config.ts    (ServiceWorkerModule / provideServiceWorker)
    

Configuration in app.config.ts

After running the schematic, the app.config.ts file is automatically updated to register the service worker. Registration only happens in production and after the application has stabilized.

app.config.ts with Service Worker registered

// app.config.ts
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideServiceWorker } from '@angular/service-worker';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    provideServiceWorker('ngsw-worker.js', {
      enabled: !isDevMode(),
      // Register SW after app stabilization
      registrationStrategy: 'registerWhenStable:30000'
    })
  ]
};
    

Service Worker and Development Environment

The service worker is disabled in development mode (isDevMode()) because aggressive caching would interfere with hot-reload and live-reloading. To test the service worker locally, you need to run a production build with ng build and serve the static files with an HTTP server like http-server or npx serve.

3. Web App Manifest

The Web App Manifest is a JSON file that describes your application to the browser and operating system. It contains information such as the app name, icons, theme colors, and display mode. Without a valid manifest, the browser will not offer the option to install the app.

Complete and optimized manifest.webmanifest

{
  "name": "My Angular PWA App",
  "short_name": "MyApp",
  "description": "A complete Angular PWA application with offline support",
  "theme_color": "#58a6ff",
  "background_color": "#0d1117",
  "display": "standalone",
  "orientation": "portrait-primary",
  "scope": "/",
  "start_url": "/",
  "id": "/",
  "categories": ["productivity", "utilities"],
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-128x128.png",
      "sizes": "128x128",
      "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"
    }
  ],
  "screenshots": [
    {
      "src": "assets/screenshots/desktop.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide",
      "label": "Main dashboard"
    },
    {
      "src": "assets/screenshots/mobile.png",
      "sizes": "750x1334",
      "type": "image/png",
      "form_factor": "narrow",
      "label": "Mobile view"
    }
  ],
  "shortcuts": [
    {
      "name": "Dashboard",
      "short_name": "Dash",
      "url": "/dashboard",
      "icons": [{ "src": "assets/icons/shortcut-dashboard.png", "sizes": "96x96" }]
    },
    {
      "name": "Settings",
      "short_name": "Settings",
      "url": "/settings",
      "icons": [{ "src": "assets/icons/shortcut-settings.png", "sizes": "96x96" }]
    }
  ]
}
    

Manifest Display Modes

Mode Description Use Case
standalone App appears as a native app without address bar Most common choice for PWAs
fullscreen App takes up the entire screen with no browser UI Games, immersive presentations
minimal-ui Similar to standalone but with minimal navigation controls Apps that need back/forward navigation
browser App opens in a standard browser tab Traditional websites (not recommended for PWAs)

4. Service Worker Configuration

The ngsw-config.json file is the heart of PWA configuration in Angular. It defines which resources are cached, how they are updated, and with which strategy. Angular automatically generates the ngsw-worker.js file during the production build based on this configuration.

ngsw-config.json with complete configuration

{
  "$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": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
        ]
      }
    }
  ],
  "dataGroups": [
    {
      "name": "api-freshness",
      "urls": ["/api/user/**", "/api/notifications/**"],
      "cacheConfig": {
        "strategy": "freshness",
        "maxSize": 50,
        "maxAge": "1h",
        "timeout": "5s"
      }
    },
    {
      "name": "api-performance",
      "urls": ["/api/articles/**", "/api/products/**"],
      "cacheConfig": {
        "strategy": "performance",
        "maxSize": 100,
        "maxAge": "1d"
      }
    }
  ],
  "navigationUrls": [
    "/**",
    "!/__**"
  ]
}
    

assetGroups vs dataGroups

Property assetGroups dataGroups
Purpose Static app resources (JS, CSS, images) Dynamic API data
Versioning Based on file hash Based on time (maxAge)
installMode prefetch or lazy N/A
strategy N/A freshness or performance
Update trigger When app version changes Based on maxAge and timeout

5. Caching Strategies

Choosing the right caching strategy is fundamental for balancing performance and data freshness. Angular NGSW supports two main strategies for dataGroups: freshness (network-first) and performance (cache-first), plus static caching for assets.

Caching Strategies in Detail

Strategy Priority Behavior Use Case
prefetch (assetGroups) Cache Downloads all resources immediately at SW installation App shell, main JS and CSS
lazy (assetGroups) Cache on demand Caches the resource only when it is first requested Images, fonts, secondary assets
freshness (dataGroups) Network Tries the network first; if timeout expires, falls back to cache User profile, notifications, real-time data
performance (dataGroups) Cache Uses cache if available and not expired; otherwise network Blog articles, product catalog, static data

The freshness strategy is equivalent to the network-first pattern: the service worker tries the network first and, if the request does not succeed within the specified timeout, serves the cached version. The performance strategy is cache-first: it immediately serves the cached version (if not expired) and updates the cache in the background.

Stale-While-Revalidate with Angular

Angular NGSW does not natively support the stale-while-revalidate strategy, but you can simulate it by combining the performance strategy with a short maxAge. For example, setting "maxAge": "5m" will serve data from cache immediately, but after 5 minutes will make a new network request on the next access. If you need more granular control, consider using the Cache API directly with a custom service worker.

6. Offline Support

One of the most important features of a PWA is the ability to work without an internet connection. Angular automatically handles caching of static resources through the service worker, but for a complete offline experience you also need to manage connection state, fallback pages, and local data persistence.

Connection State Detection

Service for monitoring online/offline state

// network-status.service.ts
import { Injectable, signal, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class NetworkStatusService {
  private platformId = inject(PLATFORM_ID);

  isOnline = signal(true);
  connectionType = signal<string>('unknown');

  constructor() {
    if (isPlatformBrowser(this.platformId)) {
      this.isOnline.set(navigator.onLine);

      window.addEventListener('online', () => {
        this.isOnline.set(true);
        this.syncPendingData();
      });

      window.addEventListener('offline', () => {
        this.isOnline.set(false);
      });

      // Network Information API (if available)
      const connection = (navigator as any).connection;
      if (connection) {
        this.connectionType.set(connection.effectiveType);
        connection.addEventListener('change', () => {
          this.connectionType.set(connection.effectiveType);
        });
      }
    }
  }

  private async syncPendingData(): Promise<void> {
    // Sync locally saved data when back online
    const pendingActions = await this.getPendingActions();
    for (const action of pendingActions) {
      try {
        await fetch(action.url, action.options);
        await this.removePendingAction(action.id);
      } catch (err) {
        console.error('Sync failed for:', action.id);
      }
    }
  }

  private async getPendingActions(): Promise<any[]> {
    // Retrieve pending actions from IndexedDB
    return [];
  }

  private async removePendingAction(id: string): Promise<void> {
    // Remove completed action from IndexedDB
  }
}
    

Data Persistence with IndexedDB

For managing structured offline data, IndexedDB is the most robust solution. Unlike localStorage (synchronous, 5-10 MB limit), IndexedDB supports large data volumes, indexed queries, and asynchronous operations.

IndexedDB service for offline persistence

// offline-storage.service.ts
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class OfflineStorageService {
  private db: IDBDatabase | null = null;
  private platformId = inject(PLATFORM_ID);
  private readonly DB_NAME = 'pwa-offline-store';
  private readonly DB_VERSION = 1;

  async init(): Promise<void> {
    if (!isPlatformBrowser(this.platformId)) return;

    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);

      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        // Create object stores for offline data
        if (!db.objectStoreNames.contains('articles')) {
          db.createObjectStore('articles', { keyPath: 'id' });
        }
        if (!db.objectStoreNames.contains('pendingActions')) {
          const store = db.createObjectStore('pendingActions', {
            keyPath: 'id', autoIncrement: true
          });
          store.createIndex('timestamp', 'timestamp');
        }
      };

      request.onsuccess = (event) => {
        this.db = (event.target as IDBOpenDBRequest).result;
        resolve();
      };

      request.onerror = () => reject(request.error);
    });
  }

  async save<T>(storeName: string, data: T): Promise<void> {
    if (!this.db) await this.init();
    return new Promise((resolve, reject) => {
      const tx = this.db!.transaction(storeName, 'readwrite');
      tx.objectStore(storeName).put(data);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  }

  async getAll<T>(storeName: string): Promise<T[]> {
    if (!this.db) await this.init();
    return new Promise((resolve, reject) => {
      const tx = this.db!.transaction(storeName, 'readonly');
      const request = tx.objectStore(storeName).getAll();
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}
    

Offline Storage Limitations

  • Browser quota: Browsers limit available storage (typically 50-80% of disk space). Use navigator.storage.estimate() to check available space
  • Eviction policy: When space is insufficient, the browser may delete data. Request persistent storage with navigator.storage.persist()
  • SSR safety: IndexedDB does not exist on the server side. Always use isPlatformBrowser before accessing it
  • Sync conflicts: When the user comes back online, local data may conflict with server data. Implement a conflict resolution strategy

7. Push Notifications

Push notifications allow your PWA to send messages to the user even when the app is not active. Angular provides the SwPush service that simplifies managing subscriptions and receiving notifications through the Web Push Protocol.

Subscribing to Push Notifications

Service for managing push notifications

// push-notification.service.ts
import { Injectable, inject } from '@angular/core';
import { SwPush } from '@angular/service-worker';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class PushNotificationService {
  private swPush = inject(SwPush);
  private http = inject(HttpClient);

  // VAPID public key (generated on server)
  private readonly VAPID_PUBLIC_KEY = 'BPxGEaVr...your-public-key...';

  async subscribeToNotifications(): Promise<void> {
    try {
      // Request permission and obtain subscription
      const subscription = await this.swPush.requestSubscription({
        serverPublicKey: this.VAPID_PUBLIC_KEY
      });

      console.log('Push subscription:', JSON.stringify(subscription));

      // Send subscription to backend
      await this.http.post('/api/push/subscribe', subscription).toPromise();
    } catch (err) {
      console.error('Push subscription error:', err);
    }
  }

  listenForNotifications(): void {
    // Notifications received while app is in foreground
    this.swPush.messages.subscribe((message: any) => {
      console.log('Notification received:', message);
      // Update the UI with new data
    });

    // Click on notification (opens/focuses the app)
    this.swPush.notificationClicks.subscribe((event) => {
      console.log('Notification click:', event.action, event.notification);
      // Navigate to the appropriate page
      const url = event.notification.data?.url;
      if (url) {
        window.open(url, '_blank');
      }
    });
  }

  async unsubscribe(): Promise<void> {
    try {
      const subscription = await this.swPush.subscription.toPromise();
      if (subscription) {
        await subscription.unsubscribe();
        await this.http.post('/api/push/unsubscribe', {
          endpoint: subscription.endpoint
        }).toPromise();
      }
    } catch (err) {
      console.error('Unsubscribe error:', err);
    }
  }
}
    

Sending Notifications from the Server

Node.js server for sending push notifications (web-push)

// server/push-server.ts (Node.js with web-push)
import webPush from 'web-push';

// VAPID configuration
webPush.setVapidDetails(
  'mailto:info@example.com',
  process.env.VAPID_PUBLIC_KEY!,
  process.env.VAPID_PRIVATE_KEY!
);

async function sendNotification(
  subscription: webPush.PushSubscription,
  payload: object
): Promise<void> {
  const notification = {
    notification: {
      title: 'New article available!',
      body: 'Discover the latest on Angular PWA',
      icon: '/assets/icons/icon-192x192.png',
      badge: '/assets/icons/badge-72x72.png',
      vibrate: [100, 50, 100],
      data: {
        url: '/blog/angular-pwa',
        dateOfArrival: Date.now()
      },
      actions: [
        { action: 'read', title: 'Read Now' },
        { action: 'close', title: 'Close' }
      ]
    }
  };

  try {
    await webPush.sendNotification(
      subscription,
      JSON.stringify(notification)
    );
  } catch (error: any) {
    if (error.statusCode === 410) {
      // Subscription expired: remove it from database
      await removeSubscription(subscription.endpoint);
    }
  }
}
    

VAPID Keys

VAPID keys (Voluntary Application Server Identification) are used to authenticate your server with the browser's push service. You can generate them with the command npx web-push generate-vapid-keys. The public key goes in the frontend, the private key stays exclusively on the server. Never commit the private key to your repository.

8. Managing App Updates

When you release a new version of your PWA, the service worker needs to download the updated resources and notify the user. Angular provides the SwUpdate service to manage the update lifecycle in a controlled manner.

Service for managing app updates

// app-update.service.ts
import {
  Injectable, inject, ApplicationRef
} from '@angular/core';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { filter, first, interval, concat } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class AppUpdateService {
  private swUpdate = inject(SwUpdate);
  private appRef = inject(ApplicationRef);

  initUpdateCheck(): void {
    if (!this.swUpdate.isEnabled) {
      console.log('Service Worker not enabled');
      return;
    }

    // Check for updates every 6 hours
    const appIsStable$ = this.appRef.isStable.pipe(
      first(stable => stable)
    );
    const every6Hours$ = interval(6 * 60 * 60 * 1000);
    const every6HoursOnceAppStable$ = concat(appIsStable$, every6Hours$);

    every6HoursOnceAppStable$.subscribe(async () => {
      try {
        const updateFound = await this.swUpdate.checkForUpdate();
        console.log(updateFound
          ? 'New update found!'
          : 'App is already up to date');
      } catch (err) {
        console.error('Update check error:', err);
      }
    });

    // Listen for when a new version is ready
    this.swUpdate.versionUpdates.pipe(
      filter((event): event is VersionReadyEvent =>
        event.type === 'VERSION_READY'
      )
    ).subscribe(event => {
      console.log('Current version:', event.currentVersion.hash);
      console.log('New version:', event.latestVersion.hash);
      this.promptUpdate();
    });

    // Handle unrecoverable SW errors
    this.swUpdate.unrecoverable.subscribe(event => {
      console.error('SW in unrecoverable state:', event.reason);
      // Force a full reload
      document.location.reload();
    });
  }

  private promptUpdate(): void {
    const shouldUpdate = confirm(
      'A new version of the app is available. ' +
      'Would you like to update now?'
    );

    if (shouldUpdate) {
      // Activate new version and reload
      this.swUpdate.activateUpdate().then(() => {
        document.location.reload();
      });
    }
  }
}
    

PWA Update Lifecycle

Phase SwUpdate Event Description
1. Detection VERSION_DETECTED The SW found a new version on the server
2. Download VERSION_INSTALLATION_FAILED Error during new version download (if failed)
3. Ready VERSION_READY New version is downloaded and ready for activation
4. Activation activateUpdate() User confirms and SW activates the new version
5. Reload location.reload() Page reloads with updated resources

Unrecoverable Service Worker Error

The unrecoverable event indicates that the service worker can no longer correctly serve the current version of the app (for example, the server has removed resources that are still needed). In this case, the only solution is to force a full page reload with document.location.reload(). Monitor this event in production to identify deployment issues.

9. Custom Install Prompt

Browsers automatically display an installation banner when the PWA meets the installability criteria. However, the native banner offers limited visibility and control. By intercepting the beforeinstallprompt event, you can create a custom installation experience with a dedicated button and a design consistent with your app.

Component for handling custom install prompt

// pwa-install.component.ts
import {
  Component, signal, inject, PLATFORM_ID, afterNextRender
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Component({
  selector: 'app-pwa-install',
  standalone: true,
  template: `
    <div class="install-banner">
      <p>Install our app for a better experience!</p>
      <button (click)="installApp()">Install</button>
      <button (click)="dismissBanner()">Not now</button>
    </div>
  `
})
export class PwaInstallComponent {
  private platformId = inject(PLATFORM_ID);

  showInstallBanner = signal(false);
  isAppInstalled = signal(false);

  private deferredPrompt: any = null;

  constructor() {
    afterNextRender(() => {
      this.setupInstallPrompt();
      this.checkIfInstalled();
    });
  }

  private setupInstallPrompt(): void {
    if (!isPlatformBrowser(this.platformId)) return;

    // Intercept the beforeinstallprompt event
    window.addEventListener('beforeinstallprompt', (event: Event) => {
      event.preventDefault();
      this.deferredPrompt = event;
      this.showInstallBanner.set(true);
    });

    // Detect when the app is installed
    window.addEventListener('appinstalled', () => {
      this.isAppInstalled.set(true);
      this.showInstallBanner.set(false);
      this.deferredPrompt = null;
      console.log('PWA installed successfully!');
    });
  }

  async installApp(): Promise<void> {
    if (!this.deferredPrompt) return;

    // Show the native install prompt
    this.deferredPrompt.prompt();

    const result = await this.deferredPrompt.userChoice;
    console.log('User choice:', result.outcome);

    if (result.outcome === 'accepted') {
      console.log('Installation accepted');
    } else {
      console.log('Installation dismissed');
    }

    this.deferredPrompt = null;
    this.showInstallBanner.set(false);
  }

  dismissBanner(): void {
    this.showInstallBanner.set(false);
  }

  private checkIfInstalled(): void {
    // Check if app is already in standalone mode
    const isStandalone = window.matchMedia(
      '(display-mode: standalone)'
    ).matches;
    this.isAppInstalled.set(isStandalone);
  }
}
    

PWA Installability Criteria

Requirement Description
HTTPS The app must be served over HTTPS (or localhost for dev)
Web App Manifest Must include name, icons (192px + 512px), start_url, display
Service Worker An active service worker with a fetch event handler
Not already installed The app must not already be installed on the device
User engagement The user must have interacted with the domain (click, scroll, etc.)

10. Testing and Debugging

Testing a PWA requires specific tools because the service worker is only active in production and PWA features are not available during development with ng serve. Chrome DevTools and Lighthouse provide everything needed to verify and debug every aspect of your PWA.

Local Build and Serving for Testing

Commands for testing the PWA locally

# 1. Production build
ng build

# 2. Install a static HTTP server (one time)
npm install -g http-server

# 3. Serve the production build
cd dist/your-app/browser
http-server -p 8080 -c-1

# Or with npx (no global install)
npx serve dist/your-app/browser -l 8080

# 4. Open browser at http://localhost:8080
# 5. Open DevTools > Application tab
    

Chrome DevTools - Application Tab

The Application tab in Chrome DevTools is the main panel for inspecting and debugging all aspects of a PWA.

DevTools Panels for PWA

Panel Function Verification
Manifest Shows the parsed manifest Name, icons, display mode, start URL are correct
Service Workers Status of registered SWs SW is active, pending updates, errors
Cache Storage Cache contents Correct resources are cached, total sizes
IndexedDB Local databases Offline data persisted correctly
Storage Storage quota and usage Space used vs available

Lighthouse PWA Audit

Lighthouse includes a dedicated section for PWA validation. The audit verifies installability criteria, offline optimizations, and best practices. A well-configured PWA should achieve a Lighthouse PWA score of 100/100.

Lighthouse PWA Checklist

  • The service worker registers a fetch handler
  • The page responds with a 200 status code when offline
  • start_url in the manifest is reachable offline
  • The manifest has name or short_name
  • The manifest has icons of at least 192px and 512px
  • The manifest specifies display (standalone/fullscreen/minimal-ui)
  • Content is sized appropriately for the viewport
  • Meta tag theme-color is present in the HTML
  • The page has a valid viewport meta tag
  • The site uses HTTPS
  • HTTP is redirected to HTTPS

Service Worker Debugging

Service Worker debugging techniques in Chrome

// In the browser, via DevTools Console:

// 1. Check service worker status
navigator.serviceWorker.getRegistrations().then(regs => {
  regs.forEach(reg => {
    console.log('SW Scope:', reg.scope);
    console.log('SW State:', reg.active?.state);
    console.log('SW Waiting:', reg.waiting);
    console.log('SW Installing:', reg.installing);
  });
});

// 2. Force service worker update
navigator.serviceWorker.getRegistrations().then(regs => {
  regs.forEach(reg => reg.update());
});

// 3. Unregister all service workers (for full reset)
navigator.serviceWorker.getRegistrations().then(regs => {
  regs.forEach(reg => reg.unregister());
});

// 4. Inspect service worker cache
caches.keys().then(names => {
  names.forEach(name => {
    caches.open(name).then(cache => {
      cache.keys().then(requests => {
        console.log(`Cache "${name}":`, requests.length, 'entries');
        requests.forEach(req => console.log(' -', req.url));
      });
    });
  });
});

// 5. Simulate offline state via DevTools
// Network tab > select "Offline" in throttling dropdown
// Or: Application > Service Workers > check "Offline"
    

Common Issues and Solutions

  • SW does not update: Use "Update on reload" in DevTools > Application > Service Workers during development
  • Stale cache persists: Clear all storage from DevTools > Application > Storage > "Clear site data"
  • Install prompt does not appear: Verify that all installability criteria are met in the Manifest panel
  • Push notifications not arriving: Check that permission is "granted" with Notification.permission and that the subscription is valid
  • CORS errors in cache: External resources must be served with appropriate CORS headers to be cached by the SW
  • SW stuck in "waiting" state: The new SW waits for all tabs of the app to close. Use skipWaiting() to force immediate activation

Summary and Next Steps

Progressive Web Apps represent a natural evolution of web applications, offering features that until recently were exclusive to native apps. Angular greatly simplifies PWA creation through its built-in support for service workers, intelligent caching, push notifications, and managed updates.

Key Concepts from This Article

  • PWA: Installable web applications with offline support, push notifications, and automatic updates
  • Setup: ng add @angular/pwa generates manifest, SW configuration, and icons
  • Manifest: JSON file describing the app (name, icons, display mode, start URL)
  • ngsw-config.json: Configuration of assetGroups (static resources) and dataGroups (dynamic API data)
  • Caching: prefetch/lazy strategies for assets, freshness/performance for API data
  • Offline: Network state detection, IndexedDB for persistence, sync when back online
  • Push: SwPush for subscriptions and receiving notifications, VAPID for server authentication
  • Updates: SwUpdate for checking, notifying, and activating new versions
  • Install prompt: beforeinstallprompt for custom installation UI
  • Testing: Chrome DevTools Application tab, Lighthouse PWA audit, SW debugging

In the next article of the series, we will explore Advanced Dependency Injection in Angular, analyzing custom injection tokens, tree-shakable providers, multi-providers, factory providers, and scoping strategies with providedIn.

Frequently asked questions

What is a Progressive Web App?

A Progressive Web App (PWA) is a web application that uses modern technologies to deliver a user experience comparable to that of a native app.

How does Angular simplify the process of creating a PWA?

Angular simplifies the process of creating a PWA through its built-in support for service workers, intelligent caching, push notifications, and managed updates.

What is the purpose of the Web App Manifest in a PWA?

The Web App Manifest is a JSON file that describes your application to the browser and operating system, containing information such as app name, icons, theme colors, and display mode.

How can you test and debug a PWA using Chrome DevTools?

You can test and debug a PWA using Chrome DevTools by inspecting and debugging all aspects of the PWA in the Application tab.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects