Progressive Web Apps

PWAs provide a cost-effective way to develop cross-platform apps. At the end of this post you should know what PWAs are and how to build a simple PWA from scratch. We also provide resources for going further into the topic.

Motivation

What are Progressive Web Apps?

To understand what progressive web apps are, we first need to take a look at the differences between the characteristics of websites on the one hand and native apps on the other hand.

Websites are always up-to-date. They can be reached by anyone on any device. They require only a single codebase. Furthermore, they are often highly accessible and responsive, as well as easy to find with search engines.

Native Apps are deeply integrated into your device. They’re on your home-screen or taskbar and work regardless of network connection. They have access to your hardware, e.g. read your files or use your Bluetooth connection. Platform-specific apps can do things like managing your contacts, control song playback, send push notifications or take pictures.

Progressive Web Apps offer all the capabilities of both worlds. They are built using web technologies, but can make use of platform-specific features and provide a native-like experience.

As you might remember, we already talked about the surprising power of web apps in an earlier blog post. If not, please feel free to check it out at any time. It offers a brief historical summary, explaining why PWAs were developed and what its use cases are.

The term “Progressive Web Apps” is used to describe any app which uses native Web APIs and the features of modern browsers. Most importantly, Service Workers and Web App Manifests.

Advantages and disadvantages

The advantages of PWAs are obvious: they offer a cross-platform, platform-specific experience with offline and background operation. They are easier maintained than multiple native apps and therefore more cost-effective. They can just as well be published on app stores like the Google Play Store, the App Store or the Microsoft Store. Not only that, but they provide automatic and seamless updates to ensure the user always has access to the latest version.

The disadvantages of PWAs are: firstly, dependency on browser support, as they rely on modern browser features. Secondly, besides most of the native APIs, there are still features exclusive to native applications, e.g. push notifications on iOS. They are resource-heavy and not suited for devices with resource limitations. Lastly, they are also not great for using very platform-specific features like the Windows API.

Another issue is the general awareness of the existence of PWAs. How often did you notice the “install app” badge at the top-right of your search bar?

Compatibility

Progressive Web Apps are supported to different degrees on all platforms. Most importantly by Chromium-based browsers on Windows, macOS, Linux and Android and to some degree, even by Safari on iOS.

Table showing compatibility of PWAs on different browsers and devicesStarting from iOS 17.4, Apple does no longer support PWAs through home screen apps. There is still an "add to home screen" option, but instead of a standalone app, it will just be a bookmark in your default browser. Also, progressive web apps are currently not available for the Apple Vision Pro.

As with everything on the web, it is important to be aware about the limitations and capabilities of PWAs and adjust accordingly.

Limitations include the lack of push notifications, integration APIs (such as Web Bluetooth or WebNFC), and installation promotion techniques that help users know they can install the current website to get an app experience” (Source)

Requirements

In order to build a PWA, you’ll need to full fill the installability requirements, create a manifest.json file and have a minimal website. Let’s start off by looking at how the browser validates if your app is a valid installable app.

Installability

The requirements for your website to be installable can be listed in short:

  • The web app is not already installed
  • The user needs to interact with your site - Clicked the page at least once - Spent at least 30 seconds on the page
  • The website is served over HTTPS
  • A manifest.json file is linked

The Web App Manifest

The manifest.json is a file, which contains metadata about your application. For example, the name of your app, the start URL, the icons for your app and a display mode. The display mode decides whether your app looks like a webpage with a search bar at the top or a standalone app.

A minimal website

You will need at least a simple index.html file with some content and a <link> to the manifest in the head of your HTML.

Furthermore, you need a way to serve your website securely over HTTPS. For development, this can be achieved by using a tunneling tool like ngrok.

PWA from scratch

Basic app

To start off, let’s create a minimal index.html and add a reference to our manifest.json . We add crossorigin="use-credentials" so it will work with ngrok later.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
        <title>Document</title>
    </head>
    <body>
        <h1>Hello, World!</h1>
    </body>
</html>

Now let’s create the manifest.json file as well.

{
    "name": "my pwa",
    "start_url": "/",
    "display": "standalone",
    "icons": [{
        "src": "icons/512.png",
        "type": "image/png",
        "sizes": "512x512"
    }]
}

Ensure you create a 512x512 icon image in icons/512.png. The pwa-asset-generator is recommended for this task.

I'll be using Visual Studio Code's Live Server extension for serving the website and ngrok for tunneling. Your site should now be installable. Debugging can be done via Chrome's 'Application' tab or the lighthouse extension.

Service Workers

If you try to open your app without a network connection, you may notice that you cannot view your HTML file. This is where service workers come in to play. There are two basic strategies we can follow:

“Offline first”: try to load the file from cache. If no file is found, fetch the file from the server.

“Network first”: try to fetch the file first. If the server is unavailable, use the cached file.

Let’s build a service worker that employs the network first strategy. Create a new file sw.js. We start by deciding which files to cache and caching them after installing the application initially.

const cacheName = "my-pwa-cache";
const files = ["/index.html", "/manifest.json", "/icons/512.png"];

self.addEventListener("install", (e) => {
    console.log("Service Worker: Installed");
    e.waitUntil(cacheFiles());
});

async function cacheFiles() {
    console.log("Service Worker: Caching Files");
    const cache = await caches.open(cacheName);
    await cache.addAll(files);
}

Just because our files are cached doesn’t mean the files are used automatically when opening our app. We have to tell the service worker how a fetch is handled like so:

self.addEventListener("fetch", async (e) => {
    e.respondWith(networkFirst(e.request));
});

async function networkFirst(request) {
    const cache = await caches.open(cacheName);

    // try to fetch from network
    console.log("Service Worker: Fetching from network");
    let response = await tryFetch(request, 200);
    if (response) {
        cache.put(request, response.clone());
        return response;
    }

    // try to fetch from cache
    console.log("Service Worker: Fetching from cache");
    response = await cache.match(request);
    if (response) {
        return response;
    }

    throw new Error("Both network and cache failed");
}

async function tryFetch(request, timeout) {
    const abortController = new AbortController();
    const timeoutId = setTimeout(() => abortController.abort(), timeout);

    try {
        return await fetch(request, { signal: abortController.signal });
    } catch {
        return null;
    } finally {
        clearTimeout(timeoutId);
    }
}

Lastly, we have to register the service worker. We can do this by adding a script to our index.html file.

<script>
    if ("serviceWorker" in navigator) {
        navigator.serviceWorker.register("./sw.js");
    }
</script>

That’s it. Our application should now be working offline. You can view your service worker from the application tab in the Chrome developer tools and view its logs by changing the log source on the top left of the console.

The service worker cannot serve a cached version of itself, because this would lead to unexpected issues. The fetch request for the service worker cannot be intercepted by any service worker's fetch handler.

Communication with the app

Some native features like sending notifications can only be invoked by a service worker. This is why a basic communication between your service worker and the app (client) is needed.

Your service worker can send messages to the client using the Client.postMessage() function. You can find an example here.

For two-way communication, the Broadcast Channel API can be used. A basic communication can look like this:

// connect to a channel
const bc = new BroadcastChannel("test");

// in the service worker
bc.postMessage("test message");

// in the app
bc.onmessage = (e) => console.log(e)

Customization (optional)

Besides the icons, there are several other customization options in the manifest. For Example, you could set a background color before the initial CSS applies and define a theme color, which then can be interpreted differently on various operating systems. The manifest also adds the ability to define “shortcuts” for your app, which is the context-menu that pops up when you right click your app on desktop or long press the app on mobile.

Obviously, there a many other different options in the manifest worth checking out. See here.

Now that we’ve created a basic PWA, the next step is to make use of the capabilities of your device. Choosing the right native API depends largely on what you want to do. A PWA without native APIs wouldn’t make much sense as it would just be an installable webpage. In the next section I will quickly go over important APIs which you can use.

Native APIs

There are a lot of useful APIs which can be utilized. A complete list can be found here. A few important APIs are described here.

Ecosystem

Lighthouse

Lighthouse can be used to view issues and possible optimizations of your web app.

A lighthouse report showing the performance of a PWA

Workbox

Using a library like workbox can speed up the development of PWAs a lot. The example for network first caching above can be recreated with much less code. It is also highly customizable at the same time. Here is an example service worker using workbox:

importScripts("https://cdnjs.cloudflare.com/ajax/libs/workbox-sw/7.0.0/workbox-sw.min.js");

workbox.routing.registerRoute(
    () => true,
    new workbox.strategies.NetworkFirst()
);

Workbox will cache requests which match the pattern defined in registerRoute() . In this case, everything.

Frontend Frameworks

By adding workbox to your frontend framework, you can start using PWAs without much effort.

For React create-react-app provides all the tools for building a PWA out of the box. See here for more information.

Angular provides a built-in service worker module. See here for more information.

There is also a plugin for vite to add PWA support quickly and with almost zero configuration.

Conclusion

Progressive Web Apps (PWAs) represent a significant shift in the web application landscape, blending the strengths of both web and native apps. While they come with their own set of challenges, such as browser dependency and limitations on certain native features, their benefits are hard to ignore. From seamless updates and cross-platform compatibility to the utilization of modern web APIs for enhanced functionality, PWAs are redefining what web applications can achieve. As we continue to explore and integrate these technologies, it's clear that the future of web development is not just about building websites, but creating immersive, app-like experiences that are accessible to a wider audience. The evolving ecosystem, including tools like Lighthouse and Workbox, and support from various frontend frameworks, makes developing PWAs more accessible and powerful, promising an exciting future for web application development.

If you’re interested in another topic, check out Choosing the correct file structure in your projects. You can find our other blog posts here.