24
What Developers Need to Know About Progressive Web Apps
In 2011, every aspect of our lives seemed to be moving to the web. Smartphone and tablet use was rising, and the recent completion of the HTML5 specification, together with nice additions such as the web manifest, seemed to pave a path to a bright future. As the hip social network Facebook bet strongly on HTML5 for their mobile application, everything was shipshape.
Then there were some unfortunate setbacks. First, the up-and-coming native app framework PhoneGap did not go anywhere. Facebook then announced they were rewriting their mobile app. HTML5 was not ready yet, they said.
Finally, as Google announced AMP — a subset of HTML5 with some custom elements — no one wanted to bet on HTML as an app platform. Failed experiments such as webOS and Mozilla’s Firefox OS set the transition back even further.
Nevertheless, ten years later, we can give this transition another trial. This time, Google is on our side. With browser APIs added in the last decade and Chrome now the default browser on Android, developers have brought together a set of web technologies under the term “progressive web app” (PWA). A PWA can be considered a true web app.
Today, PWAs are supported by most browsers, but certainly best by Chromium-based browsers such as Microsoft Edge, Brave, Opera, and Google Chrome. One of the browsers with poor support is unfortunately Apple’s Safari. Nevertheless, most features work on Safari. The biggest pain points are PWA installation and push notifications.
Let’s explore what a PWA is, how to build one, and some challenges you may face. Then, we’ll look at some PWAs in action.
According to Wikipedia, a PWA has the following characteristics:
- Progressive: They work for every user, regardless of browser choice, because they’re built with progressive enhancement as a core tenet.
- Responsive: They fit any form factor: desktop, mobile, tablet, and forms yet to emerge.
- Connectivity independent: Service workers enable offline work, or on low-quality networks.
- App-like: They feel like an app to the user with app-style interactions and navigation.
- Fresh: They are always up to date, thanks to the service worker update process.
- Safe: They are served via HTTPS to prevent snooping and ensure content hasn’t been tampered with.
- Discoverable: They are identifiable as “applications” thanks to W3C manifests and the service worker registration scope, enabling search engines to find them.
- Re-engageable: They make re-engagement easy through features like push notifications.
- Installable: They allow users to “keep” apps they find most useful on their home screen without the hassle of an app store.
- Linkable: They are easily shared via a URL and don’t require complex installation.
There are a couple of takeaways here. There is no specification or official guideline on what a PWA is or needs to have. The characteristics mentioned are all eventually part of a PWA. However, developers may disregard or implement them differently depending on the circumstances.
The most important aspect of a PWA is that it is installable. This makes the PWA truly a web app, that is, an application on a mobile phone. Ideally, this also means that the application works — at least to some extent — offline. An offline mode is a core attribute of a good PWA.
One of the most challenging aspects of an offline mode is proper cache invalidation. Since resources and HTTP requests are now cached on the client, you need to define a proper strategy, such as a network-first strategy. Ideally, the strategy depends on the kind of resource. For instance, the app can retrieve the homepage via a network-first loading strategy, but all its related resources have a hashed file name. This makes all other resources good candidates for a cache-first loading strategy.
Finally, take the progressive point seriously. We should still respect everything we learned when HTML5 was introduced, such as using unobtrusive JavaScript, using media queries for responsive design, and providing accessibility via fallback elements and Accessible Rich Internet Application (ARIA) attributes.
Let’s now put this knowledge into practice.
While an unobtrusive and responsive website represents a great basis, it is — at least by the previous section’s definition — not a PWA. Instead, the primary goal is to control the way resources and requests are performed, but how is this accomplished? While the standard XMLHttpRequest or fetch APIs may be abstracted away, there is no direct way to override how to resolve general resource fetching, such as script sources or stylesheet links.
The answer to this riddle is the service worker. By calling the register method on the navigator interface’s serviceWorker API, you can bring in another script, which runs outside the website’s ordinary browsing context. The browser then uses this script to determine various aspects — most importantly, to intercept HTTP calls.
// check for support
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('my-service-worker.js');
}
For the registration to work:
- The worker must be served over HTTPS.
- The path must be right.
- The source must be at the same origin as the app.
The service worker doesn’t handle all requests, only requests matching the worker’s scope. The scope is mainly determined by the service worker’s location, even though it might be restricted further via some registration options.
Before going into the service worker, it makes sense to look at another aspect you should usually cover when creating a PWA: the web manifest.
With HTML5, the cache manifest was born. This file — linked from the document’s root element, that is, the html tag — has a custom syntax to define what to cache and what to replace, and with what, while offline. While service workers mostly fulfill this role, the concept of a descriptive metafile is still appealing. PWAs use the web manifest to achieve exactly that.
The application links to the web manifest via a link tag, just like stylesheets:
<link href="manifest.json" rel="manifest">
To be valid, the file itself must have four values set:
- The longer, full application name (name)
- The abbreviated, short application name (short_name)
- The actual root or starting page, in case the manifest is re-used or used in a single-page application (SPA) (start_url)
- How the application should display when installed (display)
The allowed values for display are standalone or fullscreen. The interpretation of that value is fully left to the browser. Other properties, such as theme_color or background_color, are also up to the implementation in the browser or on the operating system (OS) side. Mostly, they help display the dashboard icon, and the splash screen when the app opens, more appropriately.
Most implementations require the manifest to define a 144x144 pixel icon — otherwise, the manifest is still regarded as valid, but insufficient for making the web app installable.
A full manifest could look as follows:
{
"name": "Example App",
"short_name": "ExApp",
"display": "standalone",
"start_url": "/",
"icons": [
{
"src": "/images/icon-144.png",
"sizes": "144x144",
"type": "image/png"
}
]}
Plenty of tools and resources help generate a valid and useful manifest file, such as Web App Manifest Generator.
For service workers, there are two options:
- Use an existing library or framework to give us everything we need in a service worker.
- Implement the appropriate caching and fallback commands ourselves.
The second option seems viable at first. After all, handling HTTP requests in a service worker is as simple as writing:
self.addEventListener('fetch', e => {
// Empty for now
});
Generally, these events are important. For example, if you want to cache additional files upon PWA installation, you can use the install event. In code:
self.addEventListener('install', async e => {
const cache = await caches.open('files');
cache.addAll([
'./script.js',
'./style.css',
'./assets/image.png',
]);
});
The beauty of the cache API is that you only need to supply these relative paths and the browser does the rest. The browser fetches the URLs and puts them into the cache as you’d expect.
Handling the fetch event can quickly become tedious. At first, this may not look too bad, but handling the network reliably (and using the right caching strategy) is challenging.
A network-first strategy is quite often a good starting point. Instead of using the cache for performance enhancement, you only use the cache as a fallback, that is, when going offline. A quick implementation could be:
self.addEventListener('fetch', async e => {
const req = e.request;
const cache = await caches.open('data');
try {
const res = await fetch(req);
cache.put(req, res.clone());
await e.respondWith(res);
} catch {
const res = await cache.match(req);
if (res) {
await e.respondWith(res);
} else {
e.respondWith(getFallback(req));
}
}
});
In this code sample, the getFallback function implements a cache lookup for some initially defined fallback data. Otherwise, just return “undefined.”
This quick example shows that an implementation can rapidly get out of hand. A better alternative is to use existing tools and libraries such as Workbox. With its integrations into the webpack bundler, for example, it conveniently generates a service worker that self-updates, has proper cache invalidation rules, and can be configured using a pre-selected caching strategy.
Let’s see what other challenges you may face, even when you use specialized tools to create your PWA.
PWAs have some challenges we need to tackle, most prominently:
- Cache invalidation
- Lack of APIs
- Discoverability
- Platform-native user interface (UI)
- Synchronization
Let’s look at these challenges one-by-one.
There are three main problems in IT: cache invalidation, naming, and off-by-one errors. The cache invalidation issue seems somewhat trivial with service workers initially, but may become a real issue quite fast when space is limited. Therefore, you cannot just cache every request. If you do, you may cache requests that — by definition — give only preliminary and temporary results.
One outcome of caching too many requests may be exceptions in the form of a “quota exceeded” error. These errors are quite nasty. They may not even originate from the same application, as different quotas may encounter errors on:
- The respective request, data, or cache
- The application
- The user’s profile
- The overall browser
- The overall machine
As an example, Google Chrome may allow at most 100MB for applications running in Incognito mode. In any case, the specific limits and behavior are up to the serving browser. It makes sense to be rather conservative here.
One of the great things about native mobile development is that all necessary APIs are already available. Want geofencing in the background? Done. Want to use the fingerprint sensor? Done. Want to secure private data in a special vault? Done. Although the web now has a greatly improved arsenal of APIs, including a payment API, Bluetooth API, and more, these may not be sufficient for your use case.
Furthermore, although an API exists, it may not be available yet on the target platform. Take, for instance, the payment API. On mobile platforms, the support looks decent at over 90 percent (see image below). However, since the payment API is broken into multiple areas, some parts may have less support. It makes sense to check and test before going into production.
There is, of course, always the option of packaging the PWA in a native app bundle. That way, native APIs may be usable and native app distribution can restrict the target platform.
If we regard PWAs as a web-native replacement of native apps, then we miss a central app store. Surely, this is one advantage of PWAs, too. No central gatekeeper means more flexibility, but it also means users have a challenging time finding your PWA.
Luckily, there are multiple ways out of this. For one, you can submit your PWA to an existing PWA catalogue. An alternative is to package the PWA in a native app — as before — and still go through the respective central app stores. Finally, since a PWA is still just a website, you can use your website to advertise it properly. The discoverability may not be great, but it certainly is no worse than for a standard website.
Thinking of a PWA as a replacement for a native app also impacts the overall UI and user experience (UX) design. The app should respect the native look and behavior of the underlying platform as much as possible, but how should that work? After all, there are multiple platforms, and presumably you are not even running in an app mode but rather in the standard browser shell.
At least a CSS media query exists for finding out in what mode the app is running:
@media all and (display-mode: standalone) {
body {
/* … */
}
}
For the underlying platform, the navigator.platform may be useful. Still, you must implement plenty of functions to mimic the platform behavior as closely as possible. Even then, users can recognize that the app is not native.
Going back to cache invalidation, it makes sense to ask the question: How should the app synchronize data when going back online?
Let’s say a user went offline and submitted a form. Your logic could put that submission in a queue and push it to the server when the user is back online. While this is possible using the online / offline browser event, combined with navigator.onLine, the difficulties may be in the details. Common challenges include dependencies on other requests, handling submission failures, and mitigating replay issues.
There are plenty of reasons why PWAs are attractive to developers, as well as to companies. One is definitely the ability to circumvent the walled gardens of app store vendors such as Apple and Google.
Right now, there are a handful really well-done PWA “stores” online. In contrast to the classic app stores, these are catalogues rather than system stores. One example in that category is Appscope. Its minimalistic and to-the-point design emphasizes what most PWAs try to achieve.
The website highlights quite a few great PWAs, not only from some large tech companies but also from independent developers and hobbyists. Anyone can submit an entry, and moderators review each entry before it goes live.
Although there are many examples of impressive PWAs, the best PWAs are not always obvious. Take, for instance, Microsoft Office 365 web apps such as Word and Excel. They work — though sometimes only in a reduced mode such as read-only — in all dimensions and across multiple devices. They notify you of updates, even when closed, and are always updated with the latest assets.
Many popular apps are now also represented on the web via a PWA, such as the successful start-ups Tinder, Uber, and Pinterest. Another PWA example is the next version of the open-source electronic medical record-taking system OpenMRS. While previous versions used a server-side rendered UI, their next version will ship with a new single-page application packaged as a PWA. This way, previously impossible activities, such as being offline when recording patient observations or taking notes, are now feasible.
Overall, the transition into a PWA is quite straightforward and makes sense especially for tool-like web apps.
PWAs are here to stay. They fill a growing gap and provide the flexibility and API-richness missing from the first native app replacement attempt ten years ago. Surely, it doesn’t make sense to convert every website to a PWA, but some web applications are an ideal target to become a PWA.