Understanding

https://slides.com/agesteira/understanding-pwas

Progressive Web Applications

What is a Web Application?

Client Side

Web 2.0

XMLHttpRequest

iPhone Launch

What makes an app progressive?

It loads fast enough for mobile networks

It loads even when we are offline

It is installable

 Hmmm... looks like PWAs are aiming for mobile...

 ...but this approach goes actually much beyond.

Recipes for Mobile Apps (I)

Native Apps -> Android (Java Kotlin) or iOS (Swift)

Natively-compiled Apps -> Flutter, React Native or NativeScript

Hybrid Apps -> Ionic or Cordova without Ionic

Progressive Web Apps -> Web Apps with Service Workers

Recipes for Mobile Apps (II)

Native Natively-compiled Hybrid Progressive
Single Codebase No Yes Yes Yes
Also Web No Yes Yes Yes
Also Desktop No *No No Yes
Fast ++++ ++++ ++ +++
Hardware Integration ++++ +++ ++ +
**Rough Value 8 15 12 16

* With the exception of Flutter

** Each "Yes" equals 4. Each "No" equals 0. Each "+" equals 1.

Architecture

The App Shell

PWAs load fast is because they follow an App Shell Architecture.

App Shell is the minimal HTML, CSS and JS to power the user interface.

You can think of it as the substitute of the SDK in a mobile context.

This approach relies on aggressively precaching the shell.

The magic technology to do that is called Service Workers.

Thanks to them your app can load even when offline.

User Centric Metrics

From a user's perception page loads also have lifecycles 

Text

Screenshot from the Google I/O 2017 conference.

How to install (I)

Android uses the so called Web App Install Banners.

With iOS there is no prompt. The procedure is a little different.

How to install (II)

PWA's can also be downloaded from:

Google's Play Store,

Apple's App Store

and Microsoft's Store.

A good modern example is Twitter Lite.

Maximiliano Firtman talks about this on his talk at JSConf EU.

Problem: if you install twice you will get 2 icons on your home screen!

The App Manifest

A PWA is installable if the platform has support for this:

<link rel="manifest" href="/manifest.json">
{
  "short_name": "Maps",
  "name": "Google Maps",
  "icons": [
    {
      "src": "/images/icons-192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "/images/icons-512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/?launch=pwa",
  "background_color": "#3367D6",
  "display": "standalone",
  "orientation": "landscape",
  "scope": "/maps/",
  "theme_color": "#3367D6"
}

Web Views

Stand-alone browser VS in-app browser.

In-app browser is a browser without the browser.

Its underlying technology is called Web View.

Web Views (only) render content in the context of a native app.

Non-displaying features need the system's browser rendering engine.

Native Web Views do not only exist on mobile but also on desktop.

Browser Engines

Browser engines are virtual machines essentially made of 2 parts:

  • Rendering engine
  • JavaScript engine

Different manufacturers also have different specifications.

When a Web View needs browser features it calls one of them.

The most important browser engines are:

  • Chromium for Google Chrome -> V8 JS engine
  • WebKit for Safari -> Nitro JS engine
  • Gecko for Firefox -> SpiderMonkey JS engine

PWA features on Chromium

PWAs as we know them nowadays are a Google idea.

Chromium offers full support for PWA features.

This are the most interesting ones are:

  • Offline capabilities through Service Workers
  • Installation through "Add to Home Screen"
  • Push Notifications
  • Background Sync
  • Persistent Storage through IndexedDB
  • Web Payment
  • Web Share
  • Access to camera
  • Access to audio output
  • Geolocation

PWA limitations on Webkit (I)

WebKit only offers support for Service Workers since iOS 11.3

You can this info out on the WebKit Feature Status page.

PWA limitations on Webkit (II)

So let's see what is not there on iOS from the previous list:

  • Cache Storage quota limited to 50MB for Service Workers.
  • No `beforeinstallprompt` event so we cannot create an install button.
  • `manifest.json` poor support:
    • No ​"icons" support but can use `<link rel="apple-touch-icon" href="..." />`
    • No splash screen supported so no "background_color" support.
    • No "theme_color" support but you can use `<meta name="theme-color" content="#000" />`
    • No "display" support for "fullscreen" and "minimal-ui".
    • No "orientation" support.
  • No Push Notifications.
  • No Background Sync.
  • No persistent storage and after a few weeks all your PWA files will be deleted.
  • The access to the camera is restricted for photos only.

Service Workers

JavaScript Threads

`window` in a browser.

`global` in Node.js

`self` in workers.

* If you just want to get the global object regardless of the context you need to use the `globalThis` property.

Workers

Web Workers: they offload heavy processing from the main thread.

*Worklets: they give access to low-level parts of the rendering pipeline.

Service Workers: event driven workers that act as a proxy servers.

* Houdini uses the `PaintWorklet` under the hood.

*Service Workers...

...are a replacement for the deprecated Application Cache.

...follow the the Extensible Web Manifesto philosophy.

...offer offline capabilities such as Push Notifications and Background Sync.

* Check out Jake Archibald's site about Service Workers.

...are request interceptors either to the Network or to the Cache Storage.

...only run over HTTPS or http://localhost, for security reasons.

Service Workers Lifecycle

Ensures that the page is controlled by only 1 version of the Service Worker.

It is made of 3 events:

  1. Download: the service worker is requested through a registration.
  2. Install: when the downloaded service worker file is found to be new.
  3. Activate: it allows the Service Worker to control clients (pages).

It can have 2 possible scenarios:

A. Newly created Service Worker.

B. Updated Service Worker.

*Download

Case A. No impact in case B.

/**
 * // From a main.js
 * Once there's an active service worker this execution has no impact at all
 * unless it is downloaded again.
 */
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    // Register the service worker after the page is loaded.
    // Generally not before since this could slow down this loading step.
    navigator.serviceWorker.register('/sw.js').then(registration => {
      // Registration was successful so service worker is downloaded.
      // OPTION: registration.update();
      console.log(`Service Worker registered! Scope: ${registration.scope}`);
    }, error => {
      // Registration failed so service worker is not downloaded but just discarded. 
      console.error(`Service Worker registration failed: ${error}`);
    });
  });
}

* Notice that the worker can only control the page if it is in-scope. Here`/sw.js` is our service worker file and it is located at the root of the domain. That means that its scope is the entire origin. If we had registered it at `/scope/sw.js` then the service worker would only be able to cache fetch events for those URLs that start with `/scope/`.

Install

Same for case A and case B.

/**
 * // sw.js
 * OBSERVATION: every time we update this file or any of the cached files
 * we also must update cacheName (unless we use Workbox).
 */
var cacheName = 'my-site-cache-v1';
// What we are precaching here is the App Shell.
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/main.js'
];

self.addEventListener('install', event => {
  console.log('Installing…');
  // OPTION: self.skipWaiting() instead of event.waitUntil()
  event.waitUntil(
    caches.open(cacheName)
      .then(cache => {
        // Precaching was successful so service worker is installed.
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      }, error => {
        // Precaching failed so service worker is not installed. 
        console.error(`Service Worker installation failed: ${error}`);
      })
  );
});

*Activate

Case B. **No old caches to delete in case A.

/**
 * // sw.js
 */
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then((keyList) => {
      return Promise.all(keyList.map((key) => {
        // Same cacheName that we defined before.
        if (key !== cacheName) {
          console.log('[ServiceWorker] Removing old cache', key);
          return caches.delete(key);
        }
      }));
    })
  );
});

** In case A we do not need to delete caches but of course we need the activation, too.

* But the fact that the service worker is activated that doesn't mean that the page/client that called `.register()` will be controlled. For that we will need to reload the page and it is only from this second load on when we can intercept requests. `clients.claim()` overrides this behaviour.

Fetch

Case A or B but it has to be a second load.

/**
 * // sw.js
 * `fetch` takes care of the runtime caching.
 * This is and example of the stale-while-revalidate caching strategy.
 * https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate
 */
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        const fetchPromise = fetch(event.request).then(networkResponse => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        })
        // So if there's a cached version available, use it,
        // but fetch an update for next time.
        return cachedResponse || fetchPromise;
      }
    )
  );
});

Workbox

What is Workbox?

Caching with Service Workers can get tricky.

We register our Service Worker as normal.

But in the worker thread we can start using the libraries:

Workbox is a set of libraries that simplifies that process.

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

if (workbox) {
  console.log(`Yay! Workbox is loaded 🎉`);
} else {
  console.log(`Boo! Workbox didn't load 😬`);
}

Precaching

Wraps all 3 service worker lifecycle events with very few lines of code:

workbox.precaching.precacheAndRoute([
    '/styles/index.0c9a31.css',
    '/scripts/main.0d5770.js',
    { url: '/index.html', revision: '383676' },
]);

Runtime Caching

This is what we natively did with the fetch event but now using Workbox:

workbox.routing.registerRoute(
  '/logo.png',
  handler
);

Common Runtime Caching Strategies

The Workbox Strategies package handles the most common scenarios:

  • Cache Only: the Service Worker forces a response from the cache and never from the network.
  • Network Only: the Service Worker forces a response from the network and never from the cache.
  • Cache First falling back to network: the Service Worker tries the cache first and if there is no cached response it goes to the network. But most importantly: the response from the network is cached before being passed to the browser.
  • Network First falling back to cache: the Service Worker tries the network first. If the request is successful the response is cached before being passed to the browser. If the request fails it falls back to the last cached response.
  • Stale While Revalidate: here we only use responses from the cache but we also make a call to the network and if that call is successful we cache that response for the next time.

Workshop

Made with Slides.com