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 uses by default, with Workbox. In addition, 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 of 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:

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.

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.

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 request them.

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

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.

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 will only summarize the changes here.

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 provides 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 check box 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.

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

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 of 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 for replacing 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.

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.

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.

Because a service worker still uses the disk/http cache of the browser, it's important 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 a long expiration date.

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

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 but because the filenames are not revisioned we should not serve them with an expiration date far in the future because that would make an update very difficult.

Another important aspect of serving resources is saving bandwidth. Fortunately a webserver 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 a lot of 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 (shamless self plug). First install it: npm install bread-compressor-cli -D then add a task that compresses all files in the www folder.