Home | Send Feedback

Workbox with Ionic 3 and Lazy Loaded Modules

Published: December 17, 2017  •  Updated: April 17, 2018  •  ionic3, pwa

This tutorial does not apply to Ionic 4/5 applications. Read this blog post instead: https://golb.hplar.ch/2018/06/workbox-serviceworker-in-angular-project.html

A couple of weeks ago, I presented in this blog post a way to replace the sw-toolbox service worker library, that the Ionic 3 starter templates use by default, with Workbox. Also, the blog post describes a way to create assets with hashed filenames.

Unfortunately, the solution for hashing filenames, I presented in the previous blog post, does not work for Ionic apps that use lazy loading. Lazy loading is a way to split an application into multiple modules and only load them on demand. The benefit of this is that the initial application bundle is smaller and reduces the start-up time of the app.

In this blog post, we revisit the Workbox integration, and I show you a way how to create hashed asset filenames that work with lazy loaded modules.


First, we create an example application that uses lazy loading. The tabs template is a good starting point because we can split the different tabs into their own modules.

ionic start workboxlazy tabs

This creates an application with three tabs (Home, About, and Contact). Next, we change the About and Contact tab to lazy loaded modules. We leave the Home page in the initial application bundle because it's the page that the application initially displays, and it would not benefit from lazy loading.

To create a lazy loaded module we need to create a module file. In the src/pages/about folder create a new file about.module.ts with this content:

import {NgModule} from '@angular/core';
import {IonicPageModule} from 'ionic-angular';
import {AboutPage} from "./about";

  declarations: [AboutPage],
  imports: [IonicPageModule.forChild(AboutPage)],
export class AboutModule {


Similar to the src/app/app.module.ts, this module describes declarations, imports, and providers for a specific module.
Next, we have to add the @IonicPage decorator to the page class

import {Component} from '@angular/core';
import {IonicPage} from 'ionic-angular';

 selector: 'page-about',
 templateUrl: 'about.html'
export class AboutPage {


This enables deep linking and allows the application to reference the page by name.

Do the same for the contact page. Add a module file (src/pages/contact/contact.module.ts) and add the @IonicPage decorator to the page class.

Next, we remove the references to the About and Contact page in src/app/app.module.ts. You need to remove them from the declarations and entryComponents section.

And finally, we need to change the references to the pages in src/pages/tabs.ts. We need to reference the About and Contact page by name because they live in a different module than the tabs component. The reference to the HomePage stays the same because it's part of the main application module.

import {Component} from '@angular/core';

 templateUrl: 'tabs.html'
export class TabsPage {
 tab1Root = HomePage;
 tab2Root = 'AboutPage';
 tab3Root = 'ContactPage';


Now you can start the application with ionic serve.

When you tap the first time on the About and Contact tab and observe the network requests in the browser developer tools, you see that the application loads the module bundles 0.js and 1.js. lazy loaded modules

A problem with lazy loaded modules is that users may notice a short delay between tapping and display the page on the screen because the browser has to download and parse the bundle.

To mitigate this delay, you can enable preloading in your application. Preloading eagerly fetches all the lazy loaded modules after the initial application bundle is download and started. This way, you have a smaller initial application bundle but still the benefit that the modules are instantaneous available when the user requests them.

To enable preloading, change the IonicModule import in src/app/app.module.ts to this.

IonicModule.forRoot(MyApp, {
      preloadModules: true


When you observe the network requests again, you see that the application downloads the initial application bundle (main.js), then presents the Home page and, after that, downloads the modules (0.js and 1.js). Tapping on the Contact and About tab no longer results in additional network requests. preloading

Migrating to Workbox

Next, we replace the sw-toolbox service worker library with Workbox. These changes are described in the previous blog post and don't differ if your application uses lazy loaded modules or not. So I'm only summarizing the changes here.

npm uninstall sw-toolbox
npm uninstall @ionic-native/core
npm uninstall @ionic-native/splash-screen
npm uninstall @ionic-native/status-bar

npm install workbox-cli -D
npm install workbox-sw  

Open src/index.html and remove the script tag that loads cordova.js and uncomment the code that loads the service worker.

Remove references to Ionic native in src/app/app.module.ts and src/app/app.component.ts. Create a new file copy.config.js with this content:

module.exports = {
  copyWorkbox: {
  src: ['./node_modules/workbox-sw/build/workbox-sw.js', 
  dest: '{{WWW}}/workbox-3.6.3'


Add a config option to package.json and configure the custom copy configuration

"config": {
 "ionic_copy": "./copy.config.js",


Create a new file workbox-config.js and add this code

module.exports = {
  "globDirectory": "www/",
  "globPatterns": [
  "dontCacheBustUrlsMatching": new RegExp('.+\.[a-f0-9]{10}\..+'),
  "maximumFileSizeToCacheInBytes": 5000000,
  "swSrc": "src/service-worker.js",
  "swDest": "www/service-worker.js"


Open src/service-worker.js and overwrite the existing code with the following Workbox code.

  debug: false,
  modulePathPrefix: 'workbox-3.6.3/'


Add a dist task in package.json that creates a production build of our app and adds the resources to the precache method.

"dist": "shx rm -rf www/* && npm run build --prod && workbox injectManifest",

I also added a command that deletes the www folder before it starts the build. Make sure that you install the shx library: npm install shx -D. Ionic app scripts provide the clean task (npm run clean) but that only deletes the www/build folder, and I prefer to start from an empty directory when I do a production build.

And finally, we add a web server to our project so we can test the production build.

npm install http-server -D

The following task starts the web server, uses the www folder as root and opens the browser

"open": "http-server www -o -a localhost -p 1234"

To check if everything is set up correctly, start a production build with npm run dist. Check www/service-worker.js if all resources that the application consists of are listed as parameters to the precacheAndRoute call.

When you start the application with npm run open, it should work as usual. Also, check if the app still works when the browser no longer has a connection to the server. You can either stop the web server or check the Offline checkbox in the Network tab of the Chrome browser developer tools.

When you observe the network requests, you encounter one problem with the ionicons.woff2 file. The service worker caches this resource with the name ionicons.woff2

 "url": "assets/fonts/ionicons.woff2"

but the browser tries to fetch it with the name ionicons.woff2?v=4.1.2

Because the names do not match, the service worker does not find it in the cache and requests the resource from the server. To solve that, we need to precache the resource with the name the browser requests it. Add a second call to the precacheAndRoute method. Don't overwrite the precacheAndRoute([]) line because the injectManifest task of the Workbox CLI looks for this code to inject the resources.

   "url": "assets/fonts/ionicons.woff2?v=4.1.2"


Hashed Filenames

In this section, we revisit the task of generating filenames with hashes. In the previous blog post, I implemented this with a combination of the tools hashmark and map-replace. Unfortunately, this setup does not work with lazy, loaded modules. We could easily rename the modules (0.js, 1.js, ...) with hashmark, but the problem is replacing the references to these files.

Replacing the reference to main.js is easy because the file is referenced in index.html and map-replace can do that. But the lazy loaded module filenames are backed into the vendor.js code. With some smart scripts, we could replace the filename in this file, but there is a much easier way. We tell Webpack to produce hashed file names.

First we create a new config file webpack.config.js with this content:

const useDefaultConfig = require('@ionic/app-scripts/config/webpack.config.js');
const ManifestPlugin = require('webpack-manifest-plugin');
useDefaultConfig.prod.output.filename = '[name].[chunkhash:10].js';
useDefaultConfig.prod.plugins.push(new ManifestPlugin({
  fileName: '../../build-manifest.json'
module.exports = useDefaultConfig;


This configuration depends on the ManifestPlugin that we need to install:
npm install webpack-manifest-plugin -D

The script imports the default webpack configuration from Ionic app script and changes the output filename for the production build. The parameter [chunkhash:10] tells Webpack to compute a hash based on the file content and add it to the filename. :10 limits the hash code to 10 characters. With the ManifestPlugin Webpack exports the mapping old->new filename into the file build-manifest.json. We need this information to replace the references in the index.html page.

To activate this Webpack configuration, we need to add it to the config section of the package.json file.

"config": {
 "ionic_webpack": "./webpack.config.js"


When you start a production build with npm run build --prod or npm run dist, you see all the revisioned files in www/build.

There are two things left to do. When you list the files in the www/build folder, you see a few files without a hash in their name (main.css and polyfills.js), and the script and link tag in www/index.html still point to the unhashed filenames.

For hashing the leftovers and changing the references in index.html I wrote the following script.

const fs = require('fs');
const path = require('path');
const revHash = require('rev-hash');

const manifestJSON = require('./build-manifest.json');

const wwwDir = path.resolve(__dirname, 'www');
const buildDir = path.resolve(wwwDir, 'build');
const indexPath = path.join(wwwDir, 'index.html');

const hashedRegex = new RegExp('.+\.[a-f0-9]{10}\..+');
const ignoreFiles = [];

fs.readdirSync(buildDir).forEach(file => {

  if (!hashedRegex.test(file) && !ignoreFiles.includes(file)) {
    const filePath = path.join(buildDir, file);
    const fileHash = revHash(fs.readFileSync(filePath));

    const lastDotPos = file.lastIndexOf('.');

    const newFileName = `${file.substring(0, lastDotPos)}.${fileHash}${file.substring(lastDotPos)}`;
    const newFilePath = path.join(buildDir, newFileName);
    fs.renameSync(filePath, newFilePath);
    manifestJSON[file] = `build/${newFileName}`;


let indexContent = fs.readFileSync(indexPath).toString('utf8');

Object.keys(manifestJSON).forEach(key => {
  indexContent = indexContent.replace(`build/${key}`, manifestJSON[key]);

fs.writeFileSync(indexPath, indexContent);


The script depends on the rev-hash library: npm install rev-hash -D
The script looks for files in the www/build folder without a hash in their name, computes a hash from the content, and adds the hash to the filename.

After that, the script opens the index.html file and replaces the references to the assets. For this task, it reads the build-manifest.json file from the Webpack build with the old->new filenames mapping.

To call the script we add it to our dist task

 "dist": "shx rm -rf www/* && npm run build --prod && node ./cache-busting.js && workbox injectManifest",


When you build the application with npm run dist, all the assets in www/build should now have a hash in their filename, and all the references in www/index.html should point to the correct file.

Server Configuration

Because a service worker still uses the disk/HTTP cache of the browser, it's essential that the web server not accidentally serves the index.html with a cache expiration header far in the future. This would delay updates for the app until the cache expires.

The other resources in our app can be served with an extended expiration date.

I usually use Nginx for serving my webpages. An Nginx configuration for this application could look like this.

    location /workbox {
      alias /opt/demo/workbox;
      expires -1;

      gzip on;
      gzip_buffers 16 8k;
      gzip_comp_level 1;
      gzip_http_version 1.1;
      gzip_min_length 10;
      gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/x-icon application/vnd.ms-fontobject font/opentype application/x-font-ttf application/pdf application/json;
      gzip_vary on;
      gzip_proxied any;
      gzip_static on;

      location /workbox/assets {
         expires 30d;

      location /workbox/build {
         expires max;

This configuration serves everything in the build folder with an expiration date of 20 years in the future. All these assets are revisioned, and the names change each time we create a new version of our app.

Assets in the assets folder have an expiry date of 30 days in the future. These files are mostly static resources (web fonts, images) and may never change. Still, because the filenames are not revisioned, we should not serve them with an expiration date far in the future because that would make updates very difficult.


Another critical aspect of serving resources is saving bandwidth. Fortunately, a web server like Nginx handles that transparently with the proper configuration. When a client sends the header Accept-Encoding: gzip, the server knows that the client is capable of handling compressed responses and compresses the response on the fly before it sends it back. In Nginx, you enable this with the gzip directives.

When you maintain a very popular website and your server has to handle many requests, it can make sense to precompress the resources. Instead of compressing the responses on the fly, you compress the files in your build process, and the web server no longer has to do that for each request. He can simply read the compressed file from the file system and send it back to the client. This can save some significant CPU time.

With Nginx, you enable this with the gzip_static on; directive. When a client (that supports gzip) requests a file filename.css, Nginx looks for a file with the name filename.css.gz, and when it exists, it serves the content of this file to the client. When it does not exist, Nginx compresses the content on the fly before it sends it back.

There are many ways how to produce precompressed assets. A simple way is the bread-compressor-cli npm package (shameless self-plug). First install it: npm install bread-compressor-cli -D
then add a task that compresses all files in the www folder.

"compress": "bread-compressor -a gzip www",


and then call the task from the dist task

"dist": "shx rm -rf www/* && npm run build --prod && node ./cache-busting.js && workbox injectManifest && npm run compress",


When everything works, you should find all the resources in the www and www/build folder in an uncompressed and compressed form (ending with .gz).

This concludes our journey into the process of creating a build system for an Ionic app with lazy loaded modules, a Workbox service worker, and revisioned and precompressed asset files.

You find the complete project on GitHub