Progressive web applications are the new standard in this modern era of web development. They’re pure web applications built with the very web technologies (HTML, JS & CSS) but behave as a hybrid. i.e. somewhere between web and native apps.
The concept of PWAs started booming in late 2015 and is now becoming a standard for modern web apps as companies both large and small are now adopting PWAs for their current and future projects/products. The list includes Twitter Lite & Tinder, among others. You can see some cool examples of PWAs at Adobe Experience Cloud Blog.
One important thing to know before targeting PWAs for production is that Apple currently does not support the major features of PWA such as Service Workers, Push Notifications & Installing to home screen through a browser provided prompt. You can read more about this in this informative blog post.
What is a PWA?
According to Google, PWAs should be:
- Reliable – They should load instantly even if the network conditions are not so good or if there’s no network at all.
- Fast – They should be highly interactive and provide fast user experience. I.e. smooth scrolling and animations etc.
- Engaging – They should be engaging so the user gets an immersive experience and stays connected with the app.
Apart from these characteristics, PWAs are usually responsive and should adopt to most devices and screen sizes. They’re installable on mobile devices, meaning you can have a link on your home screen to launch the app right away making it easier to access. And they work in all web browsers. In modern browsers, we’ll leverage the amazing APIs we’ll talk about later, but in older browsers, our app should still work perfectly fine, without the amazing modern stuff of course.
Let’s get started.
I already have a simple web application which searches books using the Google Books API. We will convert this into a Progressive Web App. The finalized PWA can be cloned from this code repo. I suggest you fork the repository, clone into your machine and checkout the non-pwa-app
branch to follow along with the tutorial.
The final application should look like this and can be viewed here:
The steps going forward assume that you have Git, NodeJS, and Angular CLI installed. You will also need http-server package for this tutorial.
Running the application (non-pwa)
From the root of your project, execute the command ng-serve
using terminal/ cmd and navigate to localhost:4200. You should see the app running as:
We can always test the performance of our PWA using Lighthouse and improve areas of our application lacking in performance. Here are the stats of our non-pwa:
Making the app reliable & faster
We want our application to be reliable. It should work even if the network is poor or is unavailable. Let’s see what happens if we turn off the network and run our application. To do this, open the inspector. Go to the network tab and click on Offline. Do not close the inspector.
Now, refresh the page and you will see something like this:
To avoid this happening to end users, we’ll be using Service Workers to cache our application. Then, we can use it even when there’s no network.
To achieve this, we can configure the service worker manually. I recommend using the amazing pwa tools that the Angular team has provided. We’re going to install those tools now to introduce a service worker in our application.
Now, close the chrome inspector. From your project root, execute the following command which installs the required plugins via npm/yarn in our project:
npm install --save @angular/service-worker @angular/platform-server ng-pwa-tools
…or…
yarn add @angular/service-worker @angular/platform-server ng-pwa-tools --save
Now we’ll enable service workers inside our application. By default, they’re turned off in angular-cli
when creating a project. We can enable them by executing the following command from project root which updates our angular-cli.json
:
ng set apps.0.serviceWorker=true
From now on, we’ll be using the run.sh
bash script to serve our application because Service Workers don’t work in a development environment. We’ll be creating prod builds and serving the dist
folder using http-server
package. The script now should look like this:
#!/bin/bash PATH=$PATH:$(npm bin) set -x # Production build ng build --prod # Serve cd dist http-server
The next step is to let the service worker know about our application’s routing & assets so the service worker can cache those for repeated visits. We could create a service worker manifest for this purpose but since we already have installed some amazing tools, we’re going to create this manifest automatically by letting the tool know about our application’s routing.
We’re going to add the command below in our run.sh
to do that:
./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts
This creates the routing information in the sw-manifest.json
that is being created and since our app’s entry point is app.module.ts
it traverses the whole app to gain that information. So our run.sh
looks like this now.
#!/bin/bash PATH=$PATH:$(npm bin) set -x # Production build ng build --prod # merge ngsw-manifest and copy to dist ./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts \ --out dist/ngsw-manifest.json # Serve cd dist http-server
You can execute the bash script using sh run.sh on mac and on windows you can use cmder
or git-bash
to execute the command. After that, navigate to localhost:8080 to see the app running in prod mode.
What actually happened is that it created the prod build in the dist
folder and served to localhost:8080, created a file sw-manifest.json
and copied it to the dist
folder , included a file worker-basic.min.js
which is a basic Service Worker on page load, a service worker registration script which registers this basic service worker on page load.
Below are the contents of ngsw-manifest.json
:
{ "routing": { "index": "/index.html", "routes": { "/": { "match": "exact" }, "/search": { "match": "exact" }, "^/book/[^/]+$": { "match": "regex" } } }, "static": { "urls": { "/3rdpartylicenses.txt": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "/favicon.ico": "84161b857f5c547e3699ddfbffc6d8d737542e01", "/index.html": "e8736d97b1c413fa72faaa75710ab839a0bbebea", "/inline.71440d3fb16d45c77b34.bundle.js": "1a927b41602a9c74c968b5ce9231bbc96b82da88", "/main.78e6e14ecddcde59bd02.bundle.js": "c1ee5368ceaad3adf369fbd1980e9cb65c17914a", "/ngsw-manifest.json": "7a2ae9be33b908445d8f69684aad455c2c77c227", "/polyfills.574056ad92b56d22f7bb.bundle.js": "7c10ca088d639aac9bab14723e759e22533bf718", "/styles.abfd3a51b147501a0813.bundle.css": "729a48e901590214084c2670870f0527e00dbdc3", "/sw-register.c3a9f56ebf829a0880d3.bundle.js": "6c5694fa18f55de662ff331e4d69d4521033fd80", "/vendor.e5ff14a5323fbc5ed7b3.bundle.js": "37c0e0a51b4ab2828a6073d0d5e00f91651b69a8", "/worker-basic.min.js": "2f12848c352bd6baf25f0f1d995351696ef518ff" } } }
The contents include our application’s routing as well as all the static files that can be cached for our applications. Notice the long hash against the file URLs. This hash informs the service worker when the files have been updated by us and the service worker now needs to re-cache the newer files.
Now, go to localhost:8080 and open chrome inspector, go to Application tab and you should see our service worker up and running:
Since our service worker is alive now, switch to the Network tab, and refresh the page. You should see something like this:
We’re actually caching our assets now using the service worker. Amazing, right? But this doesn’t load any of the data that we require. That’s because currently we are not caching our search API calls. But we should get some results even if we are offline.
To do that, we’ll add our own custom ngsw-manifest.json
inside the src folder in our project. The PWA tools we’re using will combine the auto-generated ngsw-manifest.json
with our custom json file and put the output into the dist folder. So let’s create the json file and put the following content inside it:
{ "dynamic": { "group": [{ "name": "books api", "urls": { "https://www.googleapis.com/books/": { "match": "prefix" } }, "cache": { "optimizeFor": "performance", "maxAgeMs": 3600000, "maxEntries": 20, "strategy": "lru" } }] }, "external": { "urls": [{ "url": "https://fonts.googleapis.com/icon?family=Material+Icons" }] } }
The Google Books API calls are configured under the dynamic
chunk as we want every request prefixing the URL https://www.googleapis.com/books/ to be cached. We can fine tune the caching technique for every one of our URL configurations for performance
(check in service worker first, if not found, fetch from server) or for freshness
(check from the server first, if not found/offline, fetch from the service worker).
We can also cache external resources like external CSS, fonts, js files that we might include from some CDN etc.
Now, if you run sh run.sh, go to the Network tab in the inspector, and then refresh your page. You will see that the service API call (to Google Books API), is cached too and the data is being received from the server:
Tada! The application is now faster and more reliable because on every repeated visit, it fetches content from the service worker saving network calls for the end-user. Try refreshing the browser a few times and see how fast the data loads providing a smooth, faster and immersive experience. Also, try the offline mode to be further amazed 😉 All the previous visits should be cached.
We have improved our app, so let’s see if the performance seems different in Lighthouse.
As you can see, we have made some progress.
*Since this post was published, Angular has introduced Service Worker, which can be used to create PWAs. We will be releasing a blog post series that will walk you through the process of creating PWAs with the new service worker.
*edited to add
Ahsan Ayaz
Related Posts
-
Creating Apps with Ionic 2
Ionic is a fantastic and established framework for creating hybrid mobile apps for both iOS…
-
Angular Elements -- Your ngComponents Everywhere
If you’re reading this, you already know Angular. So whether you’ve worked on AngularJS (1.x)…