The different shades of Progressive Web Apps

José M. Pérez

José M. Pérez / November 02, 2018

9 min read963 views

Implementing a PWA or adapting an existing site to “become a PWA” can be daunting. There are many new technologies to learn about, but you don't need to use all of them to improve your website performance and user experience greatly.

On this post I will describe several use cases where Progressive Web Apps (PWA) can be useful to make your website achieve better performance and be more reliable.

What are Progressive Web Apps

If you are a web developer you have probably heard about Progressive Web Apps (PWA). Web sites that load quick, are reliable, feel smooth and take advantage of modern APIs.

There is a lot of buzz about them, partly because Google coined the term and has been leading the implementation of the bits and pieces that composed it.

Implementing a PWA or adapting an existing site to “become a PWA” looks like a big endeavor. The same powerful marketing that has made PWA known in the developer community can be a blessing and a curse. Developers might get the wrong idea that either they use everything and make installable offline-capable push notifications-enabled websites, or they bail out. After all, PWA is just a way to coin a set of recent browser APIs and techniques, not all or nothing.

Every website's use case is different and that is alright. If installing your website as an app is not something a user would do for your site, you don't need to build that capability. You can still take advantage of some of the technologies powering PWAs. The main requirement is that your site runs on HTTPS and, in most cases, creating a ServiceWorker, which is not more than a Javascript file.

I want to describe some of the typical scenarios where you can applied elements from PWAs.

Offline mode and pre-caching

To get your site to load without a connection, the user needs to visit it and the site needs to run a Service Worker (SW)that will make content available offline. The SW can follow different strategies (eg serving cache first, making a network request first, etc). You can find strategies on Jake Archibald's Offline Cookbook and serviceworke.rs.

“Stale-while-revalidate” strategy, of the many explained on Jake Archibald's Offline Cookbook. If there's a cached version available, use it, but fetch an update for next time.

The strategies that check the cache first can be used to fetch and pre-cache top-level navigation routes and other critical resources. Thus, the user gets a faster loading and rendering experience as they click around the PWA.

Offline support is something that can be added pretty much to any site, big or small. I run a simple SW on my blog to download the main sections and the posts that the user reads.

// the cache version gets updated every time there is a new deployment
const CACHE_VERSION = 10;
const CURRENT_CACHE = `main-${CACHE_VERSION}`;

// these are the routes we are going to cache for offline support
const cacheFiles = ['/', '/about-me/', '/projects/', '/offline/'];

// on activation we clean up the previously registered service workers
self.addEventListener('activate', evt =>
  evt.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CURRENT_CACHE) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  )
);

// on install we download the routes we want to cache for offline
self.addEventListener('install', evt =>
  evt.waitUntil(
    caches.open(CURRENT_CACHE).then(cache => {
      return cache.addAll(cacheFiles);
    })
  )
);

// fetch the resource from the network
const fromNetwork = (request, timeout) =>
  new Promise((fulfill, reject) => {
    const timeoutId = setTimeout(reject, timeout);
    fetch(request).then(response => {
      clearTimeout(timeoutId);
      fulfill(response);
      update(request);
    }, reject);
  });

// fetch the resource from the browser cache
const fromCache = request =>
  caches
    .open(CURRENT_CACHE)
    .then(cache =>
      cache
        .match(request)
        .then(matching => matching || cache.match('/offline/'))
    );

// cache the current page to make it available for offline
const update = request =>
  caches
    .open(CURRENT_CACHE)
    .then(cache =>
      fetch(request).then(response => cache.put(request, response))
    );

// general strategy when making a request (eg if online try to fetch it
// from the network with a timeout, if something fails serve from cache)
self.addEventListener('fetch', evt => {
  evt.respondWith(
    fromNetwork(evt.request, 10000).catch(() => fromCache(evt.request))
  );
  evt.waitUntil(update(evt.request));
});

With great power comes great responsibility. Do not use SWs to abuse the user's data connection, downloading content they will never need, just in case.

Service Workers for Multi-page Applications

In a multi-page app (MPA), every route that a user navigates to triggers a full request of the page, along with associated scripts and styles needed, to the server.

A good example is ele.me, the biggest food ordering and delivery company in mainland China. They used a SW to precache the main routes from their site, and implemented skeleton screens that are immediately shown while loading routes that aren't cached.

Ele.me website showing placeholders or skeleton page while loadingEle.me website fully loaded

You can read more about their approach on Google Developers site and this in-depth post.

Bridging the gap between SPAs and server-side rendered sites

Web development has gone through different stages

  1. Old sites used to be server-side rendered. Only minor interactions used Javascript in the client. Every page navigation meant a full page load.
  2. Then we started creating more complex web experiences and moved all the logic (and templates) to client-side land. Everything was amazing as long as you waited a bit while staring at a blank page whose main purpose was to load a large JS bundle.
  3. We realized that maybe we needed a hybrid solution. First request server-side rendered, then the rest happening client-side.

Reaching the third step involves doing server-side rendering. If you don't want to duplicate templates and logic, the usual solution is to use NodeJS on the server. This is fun for web developers because we get to use the same language both on server and browser. In medium and large web projects it means rewriting existing working code from python/java/PHP/others to Javascript, and the typical nuances of adopting something different from the existing solution.

Server-side rendered sites have also a largely underestimated drawback. Putting content early on the screen is one thing. Making the content interactive is another. A page that renders quickly but then doesn't respond to click/touch for seconds, while loads and executes the JS behemoth, results in frustration.

In web dev terms we are improving the First Meaningful Paint ('FMP'), but we aren't making any improvement in the Time to Interactive (‘TTI').

This short version of Addy Osmani's “The cost of JavaScript“ explains it very well:

The cost of JavaScript” by Addy Osmani. If you like it, there is also a longer version of the talk.

Paul Lewis also described the situation in his post “When everything is important nothing is”, and describes it as the Uncanny Valley.

Rendering your app server-side. An image from “When everything is important nothing is”.

I have seen many cases when server-side rendering was proposed as the first step to improve the performance in a SPA. This left off the table ideas like bundle-splitting, which would have likely reduced both FMP and TTI.

[…] you should avoid SSR if you don't need it. Most modern web apps require sophisticated interaction with the UI that has to be driven by non-trivial amounts of JavaScript. If you need to write all of that JS anyway, and you don't need the first page load benefits that SSR gives you, you'll be better off just building a static app shell and avoiding the headaches of client-server code reuse, data re-hydration, and dynamic content cache invalidation. — When should I Server-Side Render? by Michael Bleigh

Single Page Applications that are not server-side rendered can take great advantage from using Service Workers. In most cases the browser requests some data and injects it in a JS template or component. This means that the browser can cache the JS code and the data independently.

The site can then follow different strategies. For instance, it can load immediately with stale data, similar to what usually happens with native apps. It can also decide to render a skeleton while the data is fetched. In any case, it makes an approach like PRPL, easy to implement.

Installable Website

You might want to make your website installable, close to what everyone knows as an “app”. For this you don't need many to use many capabilities from the web. As long as your site runs on HTTPS you just need a web manifest, an icon and a ServiceWorker. You can find documentation on Google's Developer site and MDN.

Your site doesn't need to be fully functional offline to make it installable. Take for instance Spotify's Progressive Web App.

Spotify's Web Player works as an installable PWA.

As you see, the PWA is installable, yet it doesn't have support for offline navigation nor playback. When there is no connection, the PWA shows this custom page:

Spotify web player shows an offline page when there is no internet connection

Installable PWAs are not limited to mobile. They are already supported on ChromeOS, Windows and Linux, and pretty soon on Mac too (Chrome 72+).

More features enabled by Service Workers

I have only talked about some use cases for ServiceWorkers and other elements of PWAs, but there are more. Browsers can show push notifications after the user has enabled it for a certain site, and the push notifications are delivered even when the site is not opened. This makes it ideal for reengage with users without having to wait for them to visit your site again.

You can also use background sync, ideally for schedule data sending beyond the life of the page. Think of uploading pictures or sending chat messages even after the user leaves the page.

More Resources

I love reading about real sites that have adopted PWAs and have shared their findings. I particularly like Addy Osmani's talk “Production Progressive Web Apps With JavaScript Frameworks”, showcasing some case studies from large web sites that have been integrating these technologies to improve their key metrics.

They are good examples to demonstrate that you don't need to re-architecture an existing site to use PWA goodness, and that you can apply some of these concepts and see improvements in perceived performance very quickly.