How to Build a PWA

PWA

What is a Progressive Web App (PWA)?

I'm trying to keep this post simple so I'll define what a PWA is in my own simple terms. A PWA is a web app that uses a set of browser/OS enhancements to make it feel more like a native app.

If you want a more official definition see wikipedia or Google's Web Developer site.

PWA Basics

How do you enhance a website/web app into a Progressive Web App? Here's a nice PWA checklist... but that's quite a list. Let's just start with the basics and keep it simple, eh?

To me a PWA has at least the following 4 characteristics/features:

1. Secure (served over HTTPS)

I used to think serving content over HTTPS was a difficult, expensive undertaking... and then Let's Encrypt came along and made it free... and then Netlify came along and made it crazy-simple (and still free). In fact, hosting static sites on a CDN with HTTPS custom domains is as simple as pushing a git commit thanks to Netlify. This isn't an ad, by the way. I just like simple tools that work well.

So sign up for Netlify and hook up your github/gitlab/bitbucket repo and you'll be serving your site/app over HTTPS in no time. For more details, here is netlify's own guide on deployment.

If you already have hosting setup then there is probably some tutorial a quick internet search away about setting up SSL/HTTPS. Just make sure you do your users a favor and redirect all HTTP traffic to HTTPS (Netlify does this for you, btw) so users don't ever see this:

Not Secure Connection

2. Fullscreen / Standalone

I don't think every web app should be pinned to the home screen and act like a standalone app - some web apps are rarely used and work just fine as a favorite/bookmark in the browser. But if a user wants to get to your web app faster and pin it to their home screen/desktop, then let's make it a good experience by not showing the browser "shell".

Screenshots of Background Noise, one in Safari and one in standalone
In the browser on the left, standalone on the right.

To make your web app show up "outside" the browser (like the image on the right), you just need to add a web app manifest to your page and set display to standalone. An example manifest looks like:

{
  "name": "My App",
  "scope": "/",
  "display": "standalone",
  "start_url": "./index.html",
  "theme_color": "#b859ed",
  "background_color": "#000000",
  "icons": [
    {
      "src": "./icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    }
  ]
}

And then in your HTML make sure you link to your manifest:

<html>
  <head>
    <!-- other stuff in head -->
    <link rel="manifest" href="./manifest.json" />
  </head>
  <body>
    <!-- body stuff -->
  </body>
</html>

Both Android and iOS respect the display property in your manifest so that keeps it simple. Unfortunately, iOS doesn't respect everything in the manifest, which brings us too...

3. Launch Icon

A good PWA experience involves a real icon for your app when pinned to home screen.

Screenshot of iOS homescreen
iOS uses a screenshot of your web app if you don't provide an icon.

The standard way to provide an icon is to fill out the icons array in the manifest.json like in the example manifest above. Unfortunately, iOS (even the latest iOS13 public beta) does not respect this value, so for iOS you need to add the "apple-touch-icon" link in your HTML:

<html>
  <head>
    <!-- other stuff in head -->
    <link rel="apple-touch-icon" sizes="180x180" href="./icons/logo180.png" />
  </head>
</html>

4. Service Worker

There is a lot that could be written about how to use service workers... but let's keep it simple for now. Let's just set it up so your web app will serve up the "shell" of the app from cache every time, and check for any updates in the background (so the app is up-to-date on the next launch).

I'm always wary of relying too much on 3rd party libraries in my apps, but in this situation I think workbox is a great solution.

If you want to see a hand-written service worker take a look at sw.js for Background Noise.

First register your service worker (on browsers that support it) by putting this little script in your HTML. Notice how we load the service worker after the page has been loaded so it doesn't slow down initial page load.

<html>
  <head>
    <script>
      if ("serviceWorker" in navigator) {
        window.addEventListener("load", () => {
          navigator.serviceWorker.register("./sw.js");
        });
      }
    </script>
  </head>
</html>

Now create a sw.js file:

importScripts(
  "https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"
);

if (workbox) {
  workbox.routing.registerRoute(
    /\.(?:html|js|css|png|jpg|jpeg|svg|gif)$/,
    new workbox.strategies.StaleWhileRevalidate()
  );
}

Note the workbox version is hard-coded here. Eventually you may want to check workbox's website for the latest version and see if it's worth updating.

This file is just saying all html, js, css, and image files will be served from the cache and then updated from the network in the background. You can modify the extensions regex there to capture other files types too (like mp3 for audio files). Just be careful about caching json/xml files if your web app fetches those formats from any server endpoints, because then your app would be showing stale data (even when online). Caching such data is out of scope of this post, but maybe I'll dig into it in a future post.

Example PWA

Background Noise is my go-to PWA example. The source can be found on github. I like to use this as an example because sometimes people think PWAs have to be built as SPAs (single page apps) or built with a nice front-end framework like React, but it is not so. Any website/web app (I tend to use the terms interchangeably) can be enhanced to be a PWA, even a vanilla HTML/CSS/JS web app.

Hopefully you found this post helpful, if you have any questions you can find me on Twitter.

iOS 13 PWA Improvements
How to Build a PWA without a Build Step