The Basics of Progressive Web Apps

Progressive web app (PWA) is simply a term used to describe websites that use certain web APIs, the most important of which are the service worker API and the Cache API. Simply put, a service worker is code that runs before the request for the page document is sent to the network and can intercept any and all network requests made from that domain. The Cache API is a programatic API accessible from both the page document and from the service worker. It can be used to cache HTTP requests and responses. Combined together this enables the following features:

  • Websites available offline
  • Near instant app loading
  • Page navigation at native app speed

The Pie in the Sky Just Floated Down

Web Browsers now monitor websites that cache content and have service workers. When a website meets certain criteria the browser will allow the website to be installed. From that moment on the end user can engage the website in the same way that they would engage with a native application.

For desktop computers and laptops this means that the website can now be launched as an app from the Windows Start menu or MacOS Dock. For mobile devices this means that the app is in the app drawer and system settings in the same way that any other app would be. These apps can be integrated as sources and targets into the share system of your device just like any other app. As time proceeds, we can expect that these applications to be integrated into app stores.

At one time the web was simply a way to discover linked documents. These linked documents had the power of search engines, discoverability, link-ability, and a universal development platform. However they lacked certain features of native applications like the ability to work offline, install-ability, and rich integration into the operating system. Progressive web apps attempt to provide the best of both worlds. The web platform is now enhanced with the features that at one time only native apps had. A PWA still has the power of search engines, discoverability, link-ability, and a universal development platform but now with the ability to work offline, install-ability, and rich integration into the operating system.

The web is no longer simply a network of linked documents. It is now a network of linked, fully featured applications.

Breaking Apart Expectations

I have found the hardest part of progressive web apps are the expectations that we have developed as (1) engineers, (2) product owners, and (3) end users. The web platform has evolved faster than our expectations. There was a day where the phrase “install it from the app store” was a foreign concept that made very little sense to most people. Today, “opening an app” is as natural as yawning. But it was not always this way. PWA’s open up huge areas of opportunity but require a new shift in thinking for engineers, product owners, and end users.

Engineers

Should stop thinking about the web as simple a set of documents linked with href’s. A domain is not simply an address under which one or more documents exist, but it is a unique identifier for an application. That application can certainly be accessed through a url, but can also be accessed by “opening the app” from the App Drawer, Dock, or Start menu. Very often rich user experiences can be created simply with a CDN and without heavy handed servers and APIs. The design principle is simple: keep data local until it is necessary to send it to an API.

Product Owners

Should expect more from less. Their website no longer has to be simply a website. It can be a fully functional application. This does not require a massive server infrastructure and can instead rely on the power of users devices. This might sound odd, but we take it for granted when speaking of native apps. No one designs a native app with the expectation that the app simply displays views that were rendered on a powerful server. Instead the expectation is that the features, APIs, and functionality of the users device will be used as much as possible. This could include things as simple as push notifications, data storage, and location services but could also include sharing images, pdfs, and other files between apps or sending text messages and emails.

End Users

Should think about the web as one more “app store” that can be accessed across their devices. User expectations are hard to change, and in some way progressive web apps should do little to change them. Users now understand the concept of an “app”, “installing an app”, and “opening an app”. This should not change. What should change is where they expect to install apps.

If a user installs the Facebook app from an app store and then goes to the Facebook website, they expect the same rich experience in both places. These high expectations should be maintained, with the added expectation that they can install the Facebook app, not by going to the app store, but by clicking the install button when visiting the website in the browser.

End users expect the same rich experience from the native app and from the website. Engineers and product owners should match those expectations by developing a PWA that has all the same features as the native app.

Laying the Foundations

How do we make a progressive web app? All you need is a document served at the root url and a service worker served from “/sw.js”. Do not worry too much about the contents of the service worker yet. The key thing to understand here is that you have a document that uses the navigator.servicerWorker.register method to register a service worker.

  • Serve this document from the root url
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Your App</title>
    <link rel="stylesheet" href="/main.css">
  </head>

  <body>
    <h1>Hello, World!</h1>
    <a href="/some-page.html">Some page</a>

    <script type="text/javascript">
      if ('serviceWorker' in navigator) navigator.serviceWorker.register('sw.js');
    </script>

    <script type="module" src="/main.js"></script>
  </body>
</html>
  • Serve this service worker from “/sw.js”
const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';

const PRECACHE_URLS = [
  '/',
  'main.js',
  'main.css',
  'some-page.html'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(PRECACHE)
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(self.skipWaiting())
  );
});

self.addEventListener('activate', event => {
  const currentCaches = [PRECACHE, RUNTIME];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
    }).then(cachesToDelete => {
      return Promise.all(cachesToDelete.map(cacheToDelete => {
        return caches.delete(cacheToDelete);
      }));
    }).then(() => self.clients.claim())
  );
});

self.addEventListener('fetch', event => {
  if (event.request.url.startsWith(self.location.origin)) {
    event.respondWith(
      caches.match(event.request).then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse;
        }

        return caches.open(RUNTIME).then(cache => {
          return fetch(event.request).then(response => {
            return cache.put(event.request, response.clone()).then(() => {
              return response;
            });
          });
        });
      })
    );
  }
});

With these two files you have a document and a service worker. The service worker will be installed when the user visits the website for the first time. The service worker is doing three things:

  1. When the service worker is installed it caches certain assets such as the HTML document itself and the JS and CSS needed for the app. These are cached in the “precache-v1” cache. These files will not be updated until a new version of the service worker is released. Any urls added to the precache list will be available without a network request even before the user visits the url.
  2. Additional requests made by the app will be cached in the “runtime” cache.
  3. Any requests made by the app will only go to the network if they are not already available in the cache. This includes the request for the document itself, not just XHR requests made after the page loads.

When the browser see’s that your website has a service worker and is caching files it will automatically be made available for install. For example, in Chrome desktop you will see an install button in the url bar and in Chrome mobile you will see an “Add to Home screen” popup at the bottom of the phone.

To see an example of a relatively simple progressive web app checkout trailblazer.alexlockhart.me, which is an app where you can export Jira tickets into a CSV and upload the file to create a dependency graph of the tickets. Open DevTools to inspect the source files. What is nice about applications such as this is that there is no server side rendering, logic, or APIs. They can simply be hosted on a CDN.

I wanted this blog post to be something that can help everyone understand the what, why, and how of PWA’s but also give some actionable first steps to engineers. In a follow up blog post I will dive deeper into service workers and the Cache API to see how we can use these to develop more custom experiences and applications.

Leave a Reply

avatar
  Subscribe  
Notify of