Service Workers with Angular and Module Federation

The use of the Module Federation plugin in Angular projects can often lead to challenges, particularly when such projects dynamically consume large federated code chunks from remote containers. These scenarios may result in downtime of remote servers or lags during route navigation. A viable solution to these issues is the integration of Angular service workers for caching and serving federated code.

Service workers play a crucial role in enhancing the user experience for Angular applications utilizing Module Federation. They ensure that remote containers remain accessible, even in instances where the remote server is offline. This capability is essential for maintaining application performance and user interaction, reducing load times, and smoothing transitions between routes.

Angular Service Worker

A service worker in Angular is a small JavaScript script that runs in the background of a web application. Its main job is to manage network requests, like those for loading web pages or data.

Think of a service worker as a kind of network manager. It steps in when your app tries to send or receive data. It can decide how to handle these requests. One of its key roles is to help with caching. This means it can store some of the data on the user's device. Later, if the app needs that data again, the service worker can quickly retrieve it from the cache instead of fetching it from the internet again. This makes the app faster and more reliable, especially when the internet connection is slow or unavailable.

Service workers are especially useful in Angular applications, which are often single-page applications (SPAs). Since Angular version 5.0, Angular apps have been able to easily use service workers. This helps these apps to load faster and work more smoothly, without needing complex code to handle network requests and caching.

To learn more about Angular Service Workers and their inner workings, it's recommended to refer to the official Angular documentation on the topic.

The benefits

Performance and Reliability

Caching Strategies

  • Quick Access to Resources: Service workers cache necessary resources, including JavaScript modules and other assets. This caching mechanism means that once a user has downloaded these resources, they are stored locally, leading to significantly faster load times on subsequent visits.

  • Reduced Server Load: By serving cached content, service workers decrease the amount of data fetched from the server, thus reducing server load and bandwidth usage. This is especially beneficial for applications that use Module Federation, as it can lower the overhead involved in loading modules from remote locations.

Caching Resources

In Angular, the service worker can adopt one of two primary caching strategies for managing data resources. These strategies are designed to balance the trade-offs between data freshness and response speed.

Offline Capabilities

Resilience to Network Fluctuations: Service workers enable Angular applications to function effectively even in unstable network conditions. This is particularly advantageous for Module Federation, ensuring that the app remains functional and provides a consistent user experience, regardless of network reliability.

Streamlining Application Updates

  • Background Updates: Service workers facilitate the background updating of federated modules. When a module is updated on the server, the service worker can fetch and cache the new version without interrupting the user's current session.

  • Version Management: They help maintain version consistency across federated modules, ensuring that the application runs smoothly without any dependency conflicts.

Enhancing User Experience

  • Seamless Navigation: For applications using Module Federation, service workers can prefetch necessary modules, reducing the loading time when navigating between different parts of the application. This results in a smoother, more responsive user experience.

  • Reduced Latency: Since service workers serve cached content, they play a crucial role in reducing latency, especially in applications where modules are fetched from various remote servers.

Implementing Service Workers

This section provides a concise guide on setting up service workers in Angular. For comprehensive details, refer to the official Angular documentation.

To effectively integrate service workers into an Angular application, the initial step is to transform the application into a Progressive Web App (PWA). PWAs are known for their ability to enhance user experience by providing features similar to native apps. To understand the concept of PWAs, their unique benefits, impact on business, and development methodologies, it's recommended to consult the Progressive Web Apps page on web.dev.

PWA-ing your Application

This is achieved by executing the command ng add @angular/pwa in the root directory of your application. This command performs several essential actions:

  1. Adding the Service Worker Package: It installs the @angular/service-worker package into your project, equipping it with the necessary tools to handle service workers.

  2. Service Worker Configuration: The command generates a ngsw-config.json file. This file is central to configuring service worker behavior in your application, dictating how caching is handled, among other settings.

  3. Updating the Index File: It modifies the index.html file to include a reference to the manifest.webmanifest file, which is pivotal for PWA functionality.

Having successfully set up your application to use service workers, the next step involves configuring caching strategies. This is crucial for optimizing the performance and efficiency of your application.

Strategies for Angular Service Workers

Defining the caching strategy in an Angular application involves deciding which files or assets to cache, aiming to enhance the app's performance. Service workers facilitate caching, allowing the application to handle navigation requests (made when a new URL is entered in the navigation bar) and other API URL requests, even when offline. Thus, choosing the right caching strategy is crucial and depends on the specific setup of the Angular application. It's important to note that if your application includes lazy-loaded modules, these should be incorporated into your caching strategy. The following example in the ngsw-config.json file illustrates how caching strategies might be configured:

{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "main-assets",
      "installMode": "prefetch",
      "updateMode": "lazy",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/main.js", // main file
          "/polyfills.js", // polyfills file
          "/styles.css", // styles file
          "/lazy-module-1.js", // Example of Lazy Chunk File
          "/lazy-module-2.js" // Another Lazy Chunk File
        ]
      }
    },
    {
      "name": "additional-assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(png|jpg|jpeg|svg|gif|webp|woff2|woff|ttf|otf)"
        ]
      }
    }
  ]
}

Workers and Module Federation

When working with Module Federation in Angular applications, setting up effective caching strategies for remote containers can can be complex. A thorough understanding of the required files for the remote's operation is essential to devise an appropriate caching approach.

Dynamic Loading and Dependency Management

In scenarios where remote containers are dynamically loaded, Webpack handles the downloading of necessary dependencies.

You can verify all downloaded dependencies by inspecting the Network tab in your browser's developer tools. This inspection allows you to see all the files fetched during the loading process, providing a clear view of what might need caching. Identifying all these dependencies is the first crucial step. When the remote container is dynamically loaded, Webpack fetches any required dependencies that are not already present.

Dynamic Loading and Dependency Management

Adjusting Strategies for Remote Containers

Directly caching individual files in a remote container may not be effective due to potential file name changes in new builds. A more efficient approach is to use a wildcard pattern to cache all *.js files from the remote's URL. This method is implemented in the ngsw-config.json file.

{
  "name": "RemoteAssets",
  "installMode": "lazy",
  "updateMode": "prefetch",
  "resources": {
    "urls": [
      "https://your-remote-container-url/*.js" // Using a wildcard to cache all JS files
    ]
  }
}

Understanding Configuration Parameters

  • Name: Identifies an asset group, linked with manifestHash for cache location.
  • InstallMode: Determines initial caching behavior (prefetch for immediate, lazy for on-demand).
  • UpdateMode: Dictates caching during updates (prefetch for immediate update, lazy for delayed caching).
  • Resources: Describes the cache scope, including files and/or urls.

Updating Cached Federated Chunks

Ensuring Data Freshness

Angular Service Workers include features like the SwUpdate Service and Hard Refresh methods to keep data current.

To gain a deeper understanding of the SwUpdate Service and Hard Refresh methods used in Angular Service Workers, it's recommended to consult the official Angular documentation. This resource provides comprehensive details and guidance on these specific features.

Hard Refresh Implementation Example:

function hardRefresh() {
  navigator.serviceWorker.getRegistration().then(async (registration) => {
    if (!registration) return;
    await registration.unregister();
    window.location.reload();
  });
}

This implementation ensures the application serves the most current content to users.

When performing a Hard Refresh, the following actions are executed:

  1. Unregister the Service Worker.
  2. Clear all files cached by the Service Worker.
  3. Reload the webpage.

Building and Running the Application

After configuring your caching strategies, the next steps are to build and serve your application:

ng build
http-server -p 8080 -c-1 dist/your-app-directory

Workbox for Advanced Service Worker Management

Workbox is a collection of JavaScript libraries for Progressive Web Apps. Its capabilities extend beyond what's typically offered by framework-specific solutions like Angular Service Worker (ngsw). Particularly in complex architectures such as Module Federation in Angular applications, understanding the benefits of Workbox can be pivotal for developers aiming to optimize performance and user experience beyond the standard set of tools.

Key Features of Workbox:

  1. Enhanced Flexibility and Customization with Webpack Integration: Workbox distinguishes itself with its adaptability and customizable options. Notably, it seamlessly integrates with Webpack through the workbox-webpack-plugin, aligning perfectly with the requirements of projects utilizing Webpack, such as those in Module Federation setups. This integration enables developers to harness the full potential of Workbox's features directly within their Webpack configuration, adding a layer of efficiency and precision to service worker management.

  2. Framework Agnostic: Unlike solutions tailored to specific frameworks, Workbox can be employed across various JavaScript frameworks and libraries. This versatility makes it an ideal choice for projects that span multiple frameworks or for developers seeking a more universally applicable tool.

  3. Granular Control Over Caching: Workbox provides developers with granular control over caching strategies. It allows for the writing of custom service worker scripts, offering nuanced management of resource caching and network strategies.

In the context of Module Federation, where different parts of an application may have varied caching needs and networking strategies, Workbox's flexibility and extensive feature set make it a standout choice. Its capability to handle complex scenarios and provide custom solutions aligns well with the demands of modern, sophisticated web applications.

Installation and Configuration

Step 1: Installing Workbox

Start by adding Workbox to your Angular project:

npm install workbox-webpack-plugin --save-dev

Step 2: Configuring Workbox in Webpack

Given that Module Federation heavily relies on Webpack, configure Workbox as a plugin in your Webpack configuration. This step is crucial for ensuring that the service worker strategies align with the distributed nature of Module Federation.

Example Webpack Configuration:

const { GenerateSW } = require('workbox-webpack-plugin');

module.exports = {
  // ... other webpack config relevant to Module Federation
  plugins: [
    new GenerateSW({
      // Configurations specific to your Module Federation setup
      // these options encourage the ServiceWorkers to get in there fast
      // and not allow any straggling "old" SWs to hang around
      clientsClaim: true,
      skipWaiting: true,
    })
  ],
};

Step 3: Tailoring Caching Strategies and Exposing the Workbox Service Worker

In a Module Federation setup, the correct exposure of shared resources like the Workbox service worker is crucial. Here's how to achieve this:

  1. Define Workbox as a Shared Module: In your Module Federation plugin configuration within Webpack, declare the Workbox service worker as a shared module. This step ensures that the service worker is accessible across all federated modules.

Example Module Federation Config in webpack.config.js:

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // Other Module Federation settings
      shared: {
        // Share Workbox configuration as a module
        'workbox-webpack-plugin': {
          singleton: true,
          requiredVersion: 'your-workbox-version'
        }
      }
    })
  ],
};
  1. Dynamically Import Workbox Service Worker: Utilize dynamic import capabilities to load the Workbox service worker within your Angular application. This can typically be done in the main entry file of your application.

Example of Dynamically Loading in main.ts:

import { Workbox } from 'workbox-window';

if ('serviceWorker' in navigator) {
  const wb = new Workbox('/service-worker.js');

  wb.register();
}

In this example, workbox-window is used for simplifying the service worker registration process in a client-side application. Ensure that this package is installed and included in your project dependencies. For more information on registering Service Worker in Webpack we suggest reading official documentation on the subject.

Step 4: Customize Workbox Strategies

Workbox offers a variety of strategies for caching and network requests. Configure these strategies in a service worker file to cater to your application's specific needs.

Example service-worker.js:

import { precacheAndRoute } from 'workbox-precaching';
import { NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';

// Precaching for fast load of initial resources
precacheAndRoute(self.__WB_MANIFEST);

// Example runtime caching strategies
self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    // Network-first strategy for API requests
    event.respondWith(new NetworkFirst().handle({ event }));
  } else {
    // Stale-while-revalidate for other resources
    event.respondWith(new StaleWhileRevalidate().handle({ event }));
  }
});

In this setup, Workbox provides a network-first strategy for API calls and a stale-while-revalidate strategy for other resources, ensuring efficient data fetching and caching.

Conclusion

In summary, we explored two distinct approaches to configuring service workers in Angular applications: using Angular's built-in Service Worker (ngsw) and leveraging Workbox.

  1. Angular Service Worker (ngsw): This approach offers a straightforward, Angular-centric method for integrating service workers. It's ideal for developers seeking a quick setup with minimal configuration, providing essential functionalities aligned with Angular's ecosystem.

  2. Workbox: Workbox presents a more flexible solution, allowing for customized caching strategies and integration across various frameworks. Its compatibility with Webpack makes it particularly suitable for complex architectures like Module Federation in Angular applications.

Both methods have their unique strengths. Angular's Service Worker is well-suited for standard use cases in Angular applications, while Workbox offers greater control and customization, especially in multifaceted environments. The choice depends on the specific requirements of your project and the desired level of control over the service worker's behavior.