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 PWA Rocks.
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:
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
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
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:
# Production build
ng build --prod
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.
# Production build
ng build --prod
# merge ngsw-manifest and copy to dist
./node_modules/.bin/ngu-sw-manifest --module src/app/app.module.ts \
You can execute the bash script using sh run.sh on mac and on windows you can use
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
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:
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.
In part two, we’ll be looking at how to give users the best application load experience, even with browsers that are slow to load the first screen.