From responsive website to PWA in one day

You've probably already heard about Progressive Web Apps, but just to refresh, you are simply building an application from your website. If your application is PWA compatible, users should be able to add a shortcut (link) to the app to their home screen (desktop). The app should also work to some extent off-line, which is done with a help of the Service Worker. Note that by complying with PWA rules you will improve experience for all your users! Not just for mobile users.

In this article I will talk about some basics of transforming an existing website into a PWA. I'm saying those are the basics, but in fact, within a day or two you should be able to create a fully functional PWA. You need to avoid some traps on your way, but I'll try to guide you through this minefield.

This article is part 1 of PWA series. Part 2: PWA and HTTP Caching.

Is PWA ready to use?

Let's stop briefly and talk about if this is something that can be used now and is going to stay. In other words – is it worth your time. My answer is yes, but don't take my word for it. I'll back it up with references for you and your boss/bosses :-).

Google (Chrome/Android)

Google is highly committed to PWA from the beginning. Not surprisingly, because they basically invented it... with some help from Jake Archibald. If the name doesn't ring a bell, Jake Archibald is the one whom you might know from shouting at Application Cache (PWA predecessor) at many conferences. He didn't only criticize it, but also (with help from Google) put out a solution for us to use.

Adding PWA to home screen was first implemented in Chrome 42 and later improved to current version in Chrome 57 (Paul Kinlan. "The New and Improved Add to Home screen". Developers Google. 2017). Note that Chrome for Android was first introduced in Android 4.0 Ice Cream Sandwich (Mat Smith. "Google Chrome Beta arrives on Android (video)". Engadget. 2012). Compatibility of Chrome versions are not clear, but I was able to install and test Chrome 64 on Android 4.1.2 (Xperia S).

So basically with Android 4.1+ you should be fine.

Microsoft (Edge/Windows 10)

Microsoft seems fairly committed to PWA. They made a PWA Builder to help and build PWA. They are even running bots over the network to recognize apps and add them to Windows Store. Applications should show up in Windows Store in April 2018 (Paul Thurrott. "Microsoft’s Bold Plan to Bring PWAs to Windows 10". Thurrott. 2017).

Apple (Safari/iOS)

So how about the bad boy of the Internet? Well, I might surprise you. Apple decided to implement Service Worker in its recent update to iOS (Prathik S Shetty. "Progressive web apps (PWAs) are coming to a Safari near you". Medium. 2018). Since iOS 11.3, both Web App Manifest and Service Worker should work. There are some additional effects in how Service Worker Cache behaves, it can be cleared when the app is not used, but main takeaway is that it works (Maximiliano Firtman. "PWAs are coming to iOS 11.3: Cupertino, we have a problem". Medium. 2018).

Firefox and other

I don't mention Mozilla Firefox above because although it is a major browser, Mozilla doesn't have a major system currently. But do note that crucial standards are supported by Firefox and adding apps to home screen does work on Firefox for Android.

See more on browser compatibility on MDN: Service Worker API compatibility and Web App Manifest compatibility.

What is actually required for PWA

You can find some basic guidelines e.g. from Google (what is needed to install the app) and Microsoft (PWA Minimum Requirements).

I will not go into technical depth here. What you need to know is that the bare minimum is:

  1. HTTPS. Sorry, without this Service Worker just won't work. Even for testing. But you will need https anyway (from the looks of how things are going in a year or two, it will be required for any log-in form). If you don't have a certificate for HTTPS yet, then look into Let's Encrypt (or more specifically Certbot) and see if you can use it. Using Certbot is free and relatively easy, but it does require root access to your server and a domain registration. Note that for testing you can generate self-signed certificates.
  2. Web App Manifest. This is a basic definition of the application (name, icon and such). You can use PWA Builder to generate a basic manifest for your website. It will use icons it can find and will give some clues on where you need to go next.
  3. Responsiveness/Cross-Browser. This is not really a requirement of PWA, but remember that your website will still be a website. PWA is not a magic box. If the website doesn't work in mobile Chrome, Edge, Firefox and Safari, then it probably won't work for some of your users. Keep in mind that most traffic is currently mobile and resolution ranges of devices are huge. So make it work everywhere you can.
  4. Service Worker/Offline/Caching. You need a service worker for your application to work offline. This doesn't have to be perfect. It's just your start page that should show some content. After doing caching the right way you will both make user experience better and relieve some stress from your servers. So this is really a win-win situation.

Web App Manifest quirks

There are some quirks in how manifest works. I really recommend using Lighthouse for testing your manifest. It will do some tests that will hurt your feelings ;-), but I do recommend it. Do note that you don't need to score 100% in all tests. But read the notes carefully.

If, for example, you use SVG for an icon, you might be surprised that Lighthouse will complain about missing sizes.

So according to W3C Manifest Standard this should work:

{
  "icons": [
    {
      "src": "/icon/favicon.ico",
      "sizes": "16x16 32x32"
    },
    {
      "src": "/icon/appicon.svg",
      "sizes": "33x33"
    }
  ]
}

But it might not work (or seems not to work). You still should provide sizes to match some common sizes. Like so:

{
  "icons": [
    {
      "src": "/icon/favicon.ico",
      "sizes": "16x16 32x32"
    },
    {
      "src": "/icon/appicon.svg",
      "sizes": "48x48 72x72 96x96 114x114 128x128 144x144 192x192 256x256 512x512"
    }
  ]
}

Frankly, I think this should be fixed... But for now just paste the list and you're done.

If you are lost and don't know what can be put in a manifest files, I recommend using MDN's Web App Manifest documentation. They describe each field of the manifest in detail.

Service Worker basics

What is a Service Worker

Service Worker is in fact a set of APIs (see more on MDN). What you need to know is that most of it is about caching and taking over requests. Yes, it can take over requests sent to the server and respond with something else. That's how your application can still work without the network.

It's also worth mentioning that Service Worker Cache makes Application Cache obsolete. You don't need to know what Application Cache is, but if you do know Application Cache Manifest then you might be worried that Service Worker is too much. I know I was. I admit I liked Application Cache mainly because of its simplicity. My first thought was that Service Worker does solve some problems, but it is just too complicated... It turned out I was wrong. There are some tools out there to help you get started really quickly.

How to start

It kind of depends on what you are working with. If you use some JavaScript framework (like Angular 2+) then you might want to use configuration recommended for that framework. You could also use Service Worker generator called sw-precache which should integrate nicely if you are using gulp or webpack for bulding your application.

But Service Worker is not really dependent on any framework. You can set it up manually and it will still work fine. For example you can use basic example from Google with minor tweaks.

Using basic Service Worker from Google

Basic example from Google provides caching that will be fine for any application with pre-built (merged) scripts. You should be merging your scripts already anyway so this should be fine in most cases.

  1. Just copy the "Service Worker's JavaScript" and save it as service-worker.js.
  2. Put the file in a root folder of your application.
  3. Change PRECACHE_URLS array to a list of scripts and styles that are loaded on your first page load.
  4. Register your worker script with the snippet below (this can be put in any of your existing scripts):
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

That's it. Your application should work offline already. You might need to take a moment to tweak this and add more urls to PRECACHE_URLS as required. For example, you might want to add things users can switch to, like a CSS file from alternative theme.

Scope of Service Worker

Note that Service Worker will work in all subfolders. If your static code is in a different folder then your API, then you will probably be fine. If however your app is in the main directory and API is in a subdirectory then you might in trouble.

API in a separate folder

Let's assume this is true:

https://example.com/my-static-app/ – all your static application code is here (JS, CSS, HTML)
https://example.com/my-api/  – this is API

Assuming you don't want to cache API you would place service-worker.js in my-static-app folder. My advice is to not try to cache API at first.

API in subfolder

More complicated case:

https://example.com/ – your static application code is here (JS, CSS, HTML)
https://example.com/my-api/  – this is API

In this case service-worker.js must be placed in the main (root) folder. This means anything in my-api folder will be cached. And so only first call to /my-api/user/login would be sent to the server. Subsequent requests would be served from cache. But don't panic. Adding exceptions to SW cache is easy. You just need to return early from fetch event. And as we want to add an API exception we might as well add exception for POST/PUT and similar requests. Note that attempting to cache POST requests in Chrome will result in an error (at least in Chrome 65), but most of the time you would only want to cache GET requests anyway.

self.addEventListener('fetch', event => {
    // skip non-GET requests
    if (event.request.method !== 'GET') {
        return;
    }
    // local request
    if (event.request.url.startsWith(self.location.origin + '/my-api/')) {
        const localUrl = event.request.url.replace(self.location.origin, '');
        if (localUrl.startsWith('/my-api/')) {
            return;
        }
    }
    // ...
    // ...
});

Theoretically you actually might want to cache some API requests (especially if API is used to download translations for GUI)... But trust me – you will want to cache too little rather then too much.

Service Worker Cache basics

Pre-cache vs runtime cache

You will notice that in the basic example described above there are two distinct caches – one with a precache-v1 key and the other with runtime key. This is mostly just a convention, but those caches are filled differently by the script.

  • Pre-cache is filled when the application starts (or to be more exact – after the service worker is registered). This is done in the install event. It can download things that are not initially loaded and make them available when the device is offline.
  • Runtime cache is filled after any requests return from the server. This is done in the fetch event.

Updating your PWA for users

Note that if you just leave some basic service worker lying around, you will not be able to update your application. All pre-cached files will be served from cache. Also note that it is NOT sufficient to simply change precache-v1 to precache-v2. Firstly, your users might not get a new service worker file with this settings. Secondly, the browsers still use their standard cache after the Service Worker Cache.

So how do you actually make sure new things are loaded from the server?

  1. One strategy is that you can put a version in the URL. This is called cache-busting. It can be as simple as adding a query string like ?v=1 to a script or style URL. So something like <link rel="stylesheet" href="styles.css?v=1">. The version can be a SVN revsion number, a build time-stamp or anything similar (depends on your build process).
  2. Avoid using HTTP caching headers. Changing your build process might be complicated so it is possible to change your HTTP Cache.

Testing updates

There are some edge cases you might fall into. Before we dive into caching quirks, just check if basic updates work for you. This is an update test procedure I use:

  • Clean up memory (might be required after previous tests).
    • Use CTRL+SHIFT+DELETE and check an option to only remove cache.
    • Unregister/clear the worker.
      • Chrome: DevTools -> Application -> Clear storage.
      • Firefox: about:debugging#workers -> unregister.
    • Use CTRL+SHIFT+DELETE again (just to make sure).
  • (Re)load page.
  • Change some-file.js by adding some unique message like alert("Update v2").
  • Make sure your app.js was built (if you merge your scripts).
  • Reload page. Does new alert appear? (it shouldn't)
  • Change PRECACHE version.
  • Reload page. Does new alert appear? (it should, but it probably won't on FF)
  • Try browser restart. Does new alert appear? (it should on any browser)

Do keep in mind that you must also test your production (client) server. But if this works, you should be fine.

Also note that the above shows how to make updates and how will they work for your users (except the first part). So in the worst case, your application will be updated after browser restart. I think this is acceptable.

I haven't tested how it works on Safari for the moment, so I'll try to update later. If you can try it out, please let me know.

If above test doesn't work for you – stay tuned :-). I'll post another article dedicated to PWA updating and advanced caching subjects.

Part 2: PWA and HTTP Caching.