https://slides.com/agesteira/understanding-pwas
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.
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.
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:
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;
}
)
);
});
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: