DEV Community

Cover image for The Ultimate Guide to Building Offline Angular Apps with Service Workers
bytebantz
bytebantz

Posted on

The Ultimate Guide to Building Offline Angular Apps with Service Workers

Service workers and Progressive Web Apps represent a significant advancement in web technology. By using service workers, developers can create PWAs that offer enhanced speed, offline functionality, and improved user engagement. This guide provides an in-depth look at service workers, their functionality, and how they enhance PWAs and how to implement these technologies in Angular applications.

What are Service Workers?

Background Scripts: Service workers are special scripts that run in the background of your web browser.

Persistence: These scripts stay active even after you close the tab, allowing them to handle tasks like push notifications or background syncs.

Separate Thread: They run on a different thread from your main web page, so they don’t slow down your page directly.

Service workers act as network proxies, intercepting outgoing HTTP requests and determining how to respond to them. The service worker can intercept any requests your browser makes to load things like images, files, or data from the internet whether it’s through programmatic APIs like fetch.

Handling Requests: The service worker can decide how to respond to these requests. It can:

  • Serve a cached version if available.
  • Forward the request to the network if the cached version isn’t available or up-to-date.

Adding a service worker to an Angular application is one of the steps for turning an application into a Progressive Web App (PWA).

What is a Progressive Web App (PWA)?

A PWA is a web application that uses modern web technologies to deliver an app-like experience. Key features of PWAs include:

- Offline Functionality: PWAs can work offline or with poor network conditions.
- App-like Experience: They look and feel like native apps.
- Push Notifications: PWAs can send notifications to users.
- Installable: Users can install PWAs on their devices from the browser.

Benefits of Service Workers and PWA

1. Caching: Using a service worker to cache resources enhances site speed and reliability.

2. Installable Applications: Making your site installable provides easy access for customers from their home screen or app launcher.

3. Network Reliability: PWAs improve user experience in markets with unreliable networks or expensive mobile data by providing offline functionality.

4. Seamless Offline Experience: Keeping users in your PWA when offline offers a more seamless experience compared to the default browser offline page.

Angular’s service worker follows certain rules to ensure a reliable and consistent experience for users.

  • For instance, it caches the entire application as a single unit, ensuring all files update together. This prevents users from seeing a mix of old and new versions of the application.
  • Additionally, the service worker conserves bandwidth by only downloading resources that have changed since the last cache update.

Browser Support

To benefit from the Angular service worker, your application must run in a web browser that supports service workers. To make sure your app still runs smoothly for everyone, regardless of their browser, you need to check if the Angular service worker is enabled using SwUpdate.isEnabled before your app tries to use it. You can check if SwUpdate is enabled before using it by injecting it into your component or service and then checking its isEnabled property. This simple step can help prevent errors and ensure a better experience for all users.

export class AppComponent {
  constructor(private swUpdate: SwUpdate) {
    if (this.swUpdate.isEnabled) {
      // SwUpdate is enabled, you can use it here
    } else {
      console.log('Service worker updates are not enabled.');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

HTTPS Requirement

For service workers to be registered, the application must be accessed over HTTPS, not HTTP. Browsers enforce this requirement to prevent potential security vulnerabilities. However, browsers do not require a secure connection when accessing an application on localhost.

Caching

Service workers provide access to the Cache interface, a caching mechanism separate from the HTTP cache.

Service workers utilize two caching concepts:

· Precaching:
Precaching involves caching assets ahead of time, typically during installation, improving page speed and offline access.

· Runtime caching:
Runtime caching applies caching strategies to assets requested from the network during runtime

Managing Caching

To manage caching effectively, Angular’s service worker uses a manifest file called ngsw.json. This file describes which resources to cache and includes unique identifiers (hashes) for each file’s content. When there’s an update to the application, the manifest file changes, prompting the service worker to download and cache the new version.

The ngsw.json File

Creation and Updates:

The ngsw.json file is created automatically based on a configuration file called ngsw-config.json.

The ngsw-config.json JSON configuration file specifies which files and data URLs the Angular service worker should cache and how it should update the cached files and data.

When you deploy an update to your Angular application, the contents of ngsw.json change, signaling the service worker to download the new version.

Service Worker Update Behavior:

Service workers serve the cached version for speed but periodically check for updates in the background.

When an update is available, the service worker installs it in the background and switches to the updated version on the next page load or reload.

Adding a service worker to your project

To set up the Angular service worker in your project, run the following CLI command:

ng add @angular/pwa 
Enter fullscreen mode Exit fullscreen mode

The above command:

  1. Adds the @angular/service-worker package to your project.

  2. Imports and registers the service worker with the application’s root providers.

  3. Updates the index.html file:

  • Includes a link to add the manifest.webmanifest file.
  • Adds a meta tag for theme-color.
  • Installs icon files to support the installed Progressive Web App (PWA).
  1. Creates the service worker configuration file called ngsw-config.json, which specifies the caching behaviors and other settings.

Now build and run the server using the following commands:

ng build
npx http-server -p 8080 -c-1 dist/project-name/browser
Enter fullscreen mode Exit fullscreen mode

Service worker configuration properties

appData:
This section allows you to pass additional data describing the current version of the application.

It’s commonly used to provide information for update notifications.

"appData": {
  "version": "1.0",
  "releaseDate": "2024-05-12"
}
Enter fullscreen mode Exit fullscreen mode

index:
This says which file should be shown when someone opens your app. Usually, it’s the index.html file.

"index": "/index.html"
Enter fullscreen mode Exit fullscreen mode

assetGroups:
These are groups of things your app needs, like images or scripts and their caching policies. These resources can originate from the application’s domain or external sources such as Content Delivery Networks (CDNs)

It’s recommended to organize asset groups in descending order of specificity. More specific groups should appear higher in the list. For instance, a group matching /foo.js should precede a group matching *.js.

Key Properties of an asset group

Each object in the assetGroups array adheres to the AssetGroup interface

interface AssetGroup {
  name: string;
  installMode?: 'prefetch' | 'lazy';
  updateMode?: 'prefetch' | 'lazy';
  resources: {
    files?: string[];
    urls?: string[];
  };
  cacheQueryOptions?: {
    ignoreSearch?: boolean;
  };
}
Enter fullscreen mode Exit fullscreen mode

- name: this is mandatory and serves to identify the group of
assets
- installMode Property: Determines how resources are
initially cached. It defaults to prefetch.

prefetch: Fetches and caches all listed resources while caching the current application version.

lazy: Defers caching until a resource is requested. Only caches resources that are explicitly requested, useful for conserving bandwidth.

- updateMode Property: Governs caching behavior for resources
already in the cache when a new application version is
detected. It defaults to the value of installMode.

prefetch: Immediately caches changed resources.

lazy: Defers caching of changed resources until they are requested again. This mode is applicable only if the installMode is also set to lazy.

- resources Property: Specifies files or URLs to cache.

- cacheQueryOptions Property: Modifies the matching behavior
of requests. Currently, only ignoreSearch is supported,
which disregards query parameters when matching requests.

dataGroups:
Data groups are used for caching data, like API responses.

Data groups follow this Typescript interface:

export interface DataGroup {
  name: string;
  urls: string[];
  version?: number;
  cacheConfig: {
    maxSize: number;
    maxAge: string;
    timeout?: string;
    strategy?: 'freshness' | 'performance';
  };
  cacheQueryOptions?: {
    ignoreSearch?: boolean;
  };
}
Enter fullscreen mode Exit fullscreen mode

Key Properties of a data group

- name: Uniquely identifies the data group.
- urls: Patterns that define which requests should be cached.
Only non-mutating requests (GET and HEAD) are cached.
- version: It’s like a little label attached to the data that
tells the app what version of the API it came from. This helps
the app know if the cached data matches up with the version
it’s currently using. If it doesn’t match because the API has
changed, the app knows it needs to get rid of the old cached
data and fetch fresh stuff that matches the new version of the
API.
- cacheConfig: Defines how requests matching the URLs should
be cached. It includes the following properties:
maxSize: Limits the number of cached entries or responses.

maxAge: Specifies how long a response can stay in the cache before being considered invalid.

timeout: Determines how long the ServiceWorker should wait for a network response before using a cached version.

strategy: Defines how caching should be handled. It can prioritize performance (This strategy focuses on speed. If a requested resource is already in the cache, it will be used without making a network request) or freshness (This strategy prioritizes getting the most current data. It will try to fetch the requested data from the network first. Only if the network request fails (times out) will it use the cached version) of the data.

- cacheQueryOptions Property: Modifies the matching behavior
of requests. Currently, only ignoreSearch is supported, which
disregards query parameters when matching requests.

navigationUrls:
Optional section to specify URLs that should be redirected to the index file.

"navigationUrls": [
    "/**"
]
Enter fullscreen mode Exit fullscreen mode

Navigation Request Strategy:
The navigationRequestStrategy property allows configuring how navigation requests are handled, with options like ‘performance’ and ‘freshness’.

"navigationRequestStrategy": “freshness”
Enter fullscreen mode Exit fullscreen mode

SwUpdate Service

Enabling service worker support does more than just register the service worker; it also provides services you can use to interact with the service worker and control the caching of your application.

The SwUpdate service in Angular helps manage updates for your web application by notifying you about new updates and giving you control over when to check for and activate these updates

Using SwUpdate Service

The SwUpdate service in Angular gives you access to events that indicate when the service worker discovers and installs an available update for your application. For example:

import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Injectable({ providedIn: 'root' })
export class LogUpdateService {
  constructor(private updates: SwUpdate) {
    updates.versionUpdates.subscribe((evt) => {
      switch (evt.type) {
        case 'VERSION_DETECTED':
          console.log(`Downloading new app version: ${evt.version.hash}`);
          break;
        case 'VERSION_READY':
          console.log(`New app version ready for use: ${evt.latestVersion.hash}`);
          break;
        case 'VERSION_INSTALLATION_FAILED':
          console.error(`Failed to install app version '${evt.version.hash}': ${evt.error}`);
          break;
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Checking for Updates

You can manually ask the service worker to check if any updates are available. This is useful if you want updates to occur on a schedule or if your site changes frequently. For example:

import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { interval } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CheckForUpdateService {
  constructor(private updates: SwUpdate) {
    interval(6 * 60 * 60 * 1000).subscribe(() => {
      updates.checkForUpdate().then(updateFound => {
        console.log(updateFound ? 'A new version is available.' : 'Already on the latest version.');
      }).catch(error => {
        console.error('Failed to check for updates:', error);
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above, the service checks for updates every 6 hours using the checkForUpdate() method.

Updating to the Latest Version

You can prompt the user to update to the latest version by reloading the page when a new version is ready. For example:

import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Injectable({ providedIn: 'root' })
export class PromptUpdateService {
  constructor(private swUpdate: SwUpdate) {
    swUpdate.versionUpdates.subscribe(evt => {
      if (evt.type === 'VERSION_READY') {
        if (confirm('A new version is available. Do you want to update?')) {
          document.location.reload();
        }
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The above service listens for the ‘VERSION_READY’ event and prompts the user to update.

Handling Unrecoverable States

In some cases, the application version used by the service worker might be in a broken state that cannot be recovered without a full page reload. You can handle such scenarios by subscribing to the unrecoverable event of SwUpdate. For example:

import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Injectable({ providedIn: 'root' })
export class HandleUnrecoverableStateService {
  constructor(private updates: SwUpdate) {
    updates.unrecoverable.subscribe(event => {
      alert('An error occurred that we cannot recover from. Please reload the page.');
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The above service notifies the user to reload the page if an unrecoverable error occurs.

Example

Let’s create a basic News Application to demonstrate the concepts of service workers and Progressive Web Apps (PWAs).

Run the following command to generate a new project:

ng new news-app
Enter fullscreen mode Exit fullscreen mode

Run the following command to generate new services:

ng generate service news
ng generate service check-for-update
Enter fullscreen mode Exit fullscreen mode

Run the following command to generate a new interface:

ng generate interface article
Enter fullscreen mode Exit fullscreen mode

Now, let’s modify the article.ts file in the src/app directory to implement the Article interface:

export interface Article {
    title: string;
    imageUrl: string;
    comments: string[];
  }
Enter fullscreen mode Exit fullscreen mode

Now, let’s modify the news.service.ts file in the src/app directory to implement the NewsService:

import { Injectable } from '@angular/core';
import { Article } from './article'; 

@Injectable({
  providedIn: 'root'
})
export class NewsService {
  private articles: Article[] = [
    { 
      title: 'Article 1', 
      imageUrl: 'https://source.unsplash.com/300x300', 
      comments: ['Article 1 Comment 1', 'Article 1 Comment 2', 'Article 1 Comment 3'] 
    },
    { 
      title: 'Article 2', 
      imageUrl: 'https://source.unsplash.com/300x300', 
      comments: ['Article 2 Comment 1', 'Article 2 Comment 2', 'Article 2 Comment 3'] 
    },
    { 
      title: 'Article 3', 
      imageUrl: 'https://source.unsplash.com/300x300', 
      comments: ['Article 3 Comment 1', 'Article 3 Comment 2', 'Article 3 Comment 3'] 
    },
    // Add more articles as needed
  ];

  constructor() {}

  getArticles(): Article[] {
    // Simulate fetching articles from an API
    return this.articles;
  }
}
Enter fullscreen mode Exit fullscreen mode

Run the following command to generate a new component:

ng generate component article
Enter fullscreen mode Exit fullscreen mode

Now, let’s modify the article.component.ts:

import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Article } from '../article';

@Component({
  selector: 'app-article',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './article.component.html',
  styleUrl: './article.component.css'
})
export class ArticleComponent {
  @Input()
  article!: Article;

  constructor() {}
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s modify the article.component.html

<div class="article">
  <h3>{{ article.title }}</h3>
  <img [src]="article.imageUrl" alt="Article Image" />
  <div class="comments">
    <h4>Comments</h4>
    <ul>
      <li *ngFor="let comment of article.comments">{{ comment }}</li>
    </ul>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Run the following command to generate a new component:

ng generate component news

Enter fullscreen mode Exit fullscreen mode

Now, let’s modify the news.component.ts file to use the NewsService:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NewsService } from '../news.service';
import { ArticleComponent } from '../article/article.component';
import { Article } from '../article';

@Component({
  selector: 'app-news',
  standalone: true,
  imports: [CommonModule, ArticleComponent],
  templateUrl: './news.component.html',
  styleUrl: './news.component.css'
})
export class NewsComponent implements OnInit {
  articles: Article[] = [];

  constructor(private newsService: NewsService) {}

  ngOnInit(): void {
    this.articles = this.newsService.getArticles();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s modify the news.component.html to display the news articles:

<div *ngFor="let article of articles">
    <app-article [article]="article"></app-article>
 </div>
Enter fullscreen mode Exit fullscreen mode

Now, let’s modify the app.component.ts file to use the NewsComponent and the CheckForUpdateService:

import { Component } from '@angular/core';
import { NewsComponent } from './news/news.component';
import { CheckForUpdateService } from './check-for-update.service';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NewsComponent],
  providers: [CheckForUpdateService],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'news-app';
  constructor(public checkForUpdateService: CheckForUpdateService){}
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s update the app.component.html file to remove the default content and render the news component:

<app-news></app-news>
Enter fullscreen mode Exit fullscreen mode

Run the following command to set up the Angular service worker in your project

ng add @angular/pwa
Enter fullscreen mode Exit fullscreen mode

Now lets modify the ngsw-config.json file to specify which files and data URLs the Angular service worker should cache

{
  "$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/**",
          "/media/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
        ]
      }
    }
  ],
  "dataGroups": [
    {
      "name": "unsplash-images",
      "urls": [
        "https://source.unsplash.com/300x300"
      ],
      "cacheConfig": {
        "strategy": "freshness",
        "maxSize": 50,
        "maxAge": "12h"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s modify the check-for-update.service.ts file in the src/app directory to implement the CheckForUpdateService. This service checks for updates every 6 seconds and prompts the user to reload the page if a new version is ready.

import { ApplicationRef, 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 {
  constructor(appRef: ApplicationRef, updates: SwUpdate) {
    if (updates.isEnabled) {
      const appIsStable$ = appRef.isStable.pipe(
        first((isStable) => isStable === true)
      );
      const everySixSeconds$ = interval(6 * 1000); // 6 seconds
      const everySixSecondsOnceAppIsStable$ = concat(
        appIsStable$,
        everySixSeconds$
      );

      everySixSecondsOnceAppIsStable$.subscribe(async () => {
        try {
          const updateFound = await updates.checkForUpdate();
          if (updateFound) {
            if (confirm('New version available. Load new version?')) {
              // Reload the page to update to the latest version.
              document.location.reload();
            }
          } else {
            console.log('Already on the latest version.');
          }
        } catch (err) {
          console.error('Failed to check for updates:', err);
        }
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now lets modify the app.config.ts to configure service worker using provideServiceWorker inorder to be able test in dev mode

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideServiceWorker } from '@angular/service-worker';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideServiceWorker('ngsw-worker.js', {
        enabled: true,
        registrationStrategy: 'registerImmediately'
    })]
};
Enter fullscreen mode Exit fullscreen mode

Now you can build and run the server using the following commands:

ng build
npx http-server -p 8080 -c-1 dist/news-app/browser
Enter fullscreen mode Exit fullscreen mode

Testing Offline Mode

To simulate a network issue:

  • In Chrome, open Developer Tools (Tools > Developer Tools).
  • Go to the Network tab.
  • Select Offline in the Throttling dropdown menu.

This action disables network interaction for your application.

Despite the network issue, the application should still load normally on refresh due to the service worker.

When testing Angular service workers, it’s a good idea to use an incognito or private window in your browser to ensure the service worker doesn’t end up reading from a previous leftover state, which can cause unexpected behavior.

Conclusion

This article provided a comprehensive overview of service workers and Progressive Web Apps (PWAs). We examined how service workers operate as background scripts that run independently from web pages, enhancing performance through caching and enabling offline capabilities. PWAs were highlighted for their ability to deliver app-like experiences directly from the browser, offering features such as offline functionality, push notifications, and installability.

We also covered the practical steps to integrate service workers into Angular applications, including the use of Angular CLI, managing caching strategies with ngsw-config.json, and utilizing the SwUpdate service for handling updates

To get the whole code check the link below👇👇👇
https://github.com/anthony-kigotho/NewsApp

CTA

Many developers and learners encounter tutorials that are either too complex or lacking in detail, making it challenging to absorb new information effectively.

Subscribe to our newsletter today, to access our comprehensive guides and tutorials designed to simplify complex topics and guide you through practical applications.

Top comments (5)

Collapse
 
alexandru_macavei_daa9a44 profile image
Alexandru Macavei

Thanks for the article. My current problem with my app is that if I install it as a Chrome app and when I cut off network connection, then try to start the app from the shortcut, it doesn’t properly show it offline. Works only if I do the offline option in the devtools. And yes, I have started it online first so that it has the chance to cache.

Collapse
 
bytebantz profile image
bytebantz

I appreciate your feedback
Honestly I'm really not sure what might be the problem but there is a chance your Service Worker is not being registered correctly

Collapse
 
oleksandr profile image
Oleksandr • Edited

Hey
Thank you for the article!
But I don't see ngsw-worker.js file in a repo
Did I miss something?

Collapse
 
bytebantz profile image
bytebantz

I appreciate your response

The ngsw-worker.js is not included in your repo because it is generated during the build process. Try checking it on your dist folder after building your project

Collapse
 
oleksandr profile image
Oleksandr

Thank you for answer
You are right
My mistake was that I ran ""watch": "ng build --watch --configuration development" and then just stop it and tried to run http-server form dist.