In 2015, designer Frances Berriman and Google Chrome engineer Alex Russell coined the term “Progressive Web Application” or PWA. This is a concept that seeks to blur the line between native applications and web-based applications, effectively combining the ease of use of the web with the capabilities of native apps.

One of the many advantages is that there is no need for an app store of any kind to install these. You can install these PWAs straight from the browser, as long as the used browser supports this. They tend to have a smaller footprint than native apps, and there are a lot of features in development to give them more access to certain capabilities native apps already have. More about this further down.

Web apps need to adhere to certain criteria for them to be considered PWAs.
Put shortly, they should be fast, integrated, reliable and engaging, which led to the mnemonic FIRE.

So, what makes a FIRE application?

  • Fast:
    The application should be performant and initial load time should be minimal. This can be achieved by using the App Shell Model. You should define a minimal layout for your web app called the application shell that will be cached to make the load time of the app blazingly fast. 
  • Integrated:
    To be able to “install” your web app, for it to live seamlessly next to other native applications, the browser needs some information. This is where the web app manifest comes in. This simple JSON file contains important details, like the app name, icon, start URL, display mode and more. This install option is often abbreviated as A2HS: add to home screen. It adds your web app to the device’s home screen with the name and icon you defined in the manifest file.
  • Reliable:
    The biggest downside of standard web apps is that they only work when you have an active and relatively stable internet connection. To alleviate this problem, service workers were introduced. These are scripts that the browser will run in the background, separate from the web app. They can be viewed as a programmable network proxies, being able to hook into any resource fetch request the web app makes. By caching the most important assets when initializing the service worker, subsequent requests will use these cached assets instead of retrieving them from the webserver, allowing for a complete offline experience.
  • Engaging:
    To increase interaction with the user, especially on mobile devices, the Push and Web Notification API have been developed to allow PWAs to show native user notifications, if the user allows it.

Getting started

Disclaimer: All examples below have been tested on Chrome 84.  Some of the more experimental features might require you to activate a flag in your browser, for Chrome this is chrome://flags/#enable-experimental-web-platform-features.
Some of these features are prone to change, so make sure you look up the most recent draft.

Now it’s time to dig into the technical bits. As you may have derived from the previous part, there are a few requirements to turn an application into a FIRE PWA. 

For starters, your app needs a manifest file to describe some of the properties it needs to be installed.
It has to be a JSON file, but name it whatever you will. I added the following as manifest.json to my web app.

    "short_name": "PWA demo",
    "name": "Continuum PWA demo",
    "icons": [{
            "src": "/images/icon-192.png",
            "type": "image/png",
            "sizes": "192x192"
    "start_url": "/",
    "display": "standalone"
  • short_name & name: The name of the application, used for the shortcut on the home screen and the eventual title bar when installed. If there is not enough space to fit the name (as determined by the OS), short_name will be used instead.
  • icons: A list of icons the app will use as shortcut image, or to display in the taskbar.
  • start_url: When launched from its shortcut, the app will open with this URL. So if for example your start_url is /landing-page, when the app is opened from the home screen it will start on your-website-url.something/landing-page.
  • display: Determines how the application will be displayed when launched from its shortcut. In the above example, standalone means it will look and feel like a standalone application. It will have a window separate from the browser, with none of the browser UI.

There are loads more options available to tweak the settings of your web app. Check them out at

To make your application use this file, add the following to your HTML file.

<link rel="manifest" href="/manifest.json" />

Next thing on the list: a service worker. This is a JavaScript file (I named it sw.js) that, when installed for the first time by your browser, will live in the background until removed. Here, you will cache the important bits of your app.

You need to configure it to react to two events to get started.
First, there is the install event, triggered when your application is opened for the first time in the browser and no previous servicer worker is found. This is where you set up the local cache:

const CACHE_NAME = "cache-v1";
const assets = [
self.addEventListener("install", event => {
    event.waitUntil( => cache.addAll(assets)));

The event.waitUntil will wait until the provided promise completes. If this promise fails to complete, the install will also fail.  The Cache API is then used to fetch and cache all provided URLs.

On subsequent visits, the browser will, for each asset that it needs to load, query the service worker through the fetch event if configured. Here you decide what result to return for each request, most likely the cached results from the install step.

self.addEventListener("fetch", event => {
        caches.match(event.request).then(response => {
            if (response) return response;
            return fetch(event.request).then(response => {
                const responseClone = response.clone();
                    .then(cache => cache.put(event.request, responseClone));
                return response;

Here, you intercept the browser’s fetch request. The call to event.respondWith prevents the default fetch event from firing, so you can decide which resource to return. In the above example, I check if the cache already contains an entry for this resource. If found, this cached resource is returned without querying the network. Otherwise, the resource is fetched over the network, cached for subsequent calls, and then returned.

Notice how it will always return the cached resource, and only queries the network if the resource wasn’t cached yet. There are many more caching strategies, like only allowing cached resources, or showing the cached version and querying the network for an updated version. It all depends on how often these assets are updated, so choose this strategy wisely.

To register a service worker, do the following in your main JavaScript file or script tag:

if ("serviceWorker" in navigator) {
    window.addEventListener("load", () => {

For your app to be installable, the browser imposes a few criteria.
These differ between vendors, but the core criteria are the same:  

  1. You need a web app manifest, with the required options as mandated by the browser
  2. It must be served over HTTPS

Then, depending on the extra browser specific criteria, it will display or provide an option to install the app. The current spec specifies an event the browser throws if installation is possible, so you can intercept the browsers default way of showing you can install the app, and allows you to provide a custom pop-up or button. The following goes into your main JavaScript file or script tag:

let deferredPrompt;
window.addEventListener("beforeinstallprompt", event => {
    deferredPrompt = event;
document.getElementById("addButton").addEventListener("click", () => {
    deferredPrompt.userChoice.then(choiceResult => {
        if (choiceResult.outcome === "accepted") {
            console.log("User accepted the A2HS prompt");
        } else {
            console.log("User dismissed the A2HS prompt");
        deferredPrompt = null;

Lastly, notifications. Through the Web Notification and Push API, it is possible to show native notifications and have them appear even when the application is not actively running. These two APIs work in tandem to provide a native notification experience to the user.

The general idea is:

  1. Get user permission to send notifications.
  2. Create a PushSubscription by subscribing to the service worker’s pushManager. This object contains the information, like an endpoint address, so you can send events to the service worker.
  3. Send this PushSubscription to the server which will handle sending events.
  4. The server uses the Webpush protocol to send events to the PushSubscription’s endpoint.
  5. The service worker receives these events as push events, for which you can register an event handler.
  6. You can then show the notification by calling the service worker’s showNotification method.
    E.g. self.registration.showNotification(title, options) from within the service worker script.

Both the Push API and the Webpush protocol are still drafts at this point, yet are already supported by some (mostly mobile) browsers.

This feature is rather extensive, so I will refer to  for some solid examples.

Be aware that, even though the user allowed your web app to show notifications, this permission might still be overruled by an OS setting limiting notifications or interfering with the way programs run in the background. So even though your app is sending notifications left and right, the OS might not show them.

Closing the app gap

With this slogan, Google set off to bring more native functionality to the web platform.
A few of those are already available in Chrome, albeit behind an experimental flag. Again: some of these features might have their API changed, so always double check the current draft.

The Async Clipboard API allows you to manipulate the clipboard, including intercepting the copy and paste events. Using it is as simple as:

async () => {
    await navigator.clipboard.writeText("Testing the new clipboard API");
    let text = await navigator.clipboard.readText();

Aside from text, images are also supported:

async () => {
    const imageData = await fetch("/images/icon-512.png");
    const imageBlob = await imageData.blob();
    await navigator.clipboard.write([
        new ClipboardItem({
            [imageBlob.type]: imageBlob

The Web Share and Web Share Target API are next. These two APIs allow you to use the native share feature of a device to share text, URLs or even images.
Using navigator.share(shareObject), you can open the native share window. The selected target is responsible for handling the information contained in shareObject.

        title: "Share Title",
        text: "Description",
        url: "https://URL-to-share.extension",
    .then(() => console.log("Successful share"))
    .catch((e) => console.log("Error sharing", e));

To define a PWA as a target for sharing through the Web Share Target API, you need to add a share_target to your manifest.json. Here you specify which URL to open when a share is directed at your app and which parameters will be shared. These parameters will be available as query parameters to the app, which can  then be processed to complete the share action.

"share_target": {
    "action": "/share-target/",
    "method": "GET",
    "enctype": "application/x-www-form-urlencoded",
    "params": {
        "title": "title",
        "text": "text",
        "url": "url"

Another fun feature is the Badging API, which allows you to add a number notification as badge to the application icon of an installed PWA.

navigator.setAppBadge(counter).catch((e) => console.error("Something went wrong", e));
navigator.clearAppBadge().catch((e) => console.error("Something went wrong", e));

The OS decides how to display the badge, on Windows 10 it looks like this:

The list goes on, too many to explain in this post. But if you’re interested in knowing more, be sure to check out

Even more proposals

There are so many other interesting new proposals that are worth checking out, like:

  • import-maps (
    Without any kind of third-party bundler for your web app, JavaScript import statements can only resolve absolute or relative URLs to a module. With import-maps, you can specify in your HTML file a list of name-location mappings, to allow import statements to also work on these names.
  • built-in modules (
    Extends JavaScript with a set of standard modules that can be imported as any third-party module, with the difference being that standard modules will ship with the browser and won’t have to be downloaded separately.
  • Media Queries Level 5 (
    Multiple different media queries have been or will be added to query some OS or browser settings to optimize an app’s experience.
    Some highlights:
    – prefers-color-scheme: allows different CSS styles depending on the system’s light/dark preferences.
    – prefers-reduced-motion: reduce or remove animations if the system indicates the user wishes less or no animation.
    – scripting: change CSS depending on whether scripting (like JavaScript) is enabled.

Many exciting new features are popping up on the horizon. If you want to read more about anything written above, or want to stay up to date with new developments, take a look at the following resources:

Andy Dirkx

Andy Dirkx

Java Software Crafter


Andy Dirkx (24) behaalde een Bachelor Toegepaste Informatica aan de PXL met als afstudeerrichting “Applicatie Ontwikkeling”. Op dit moment werkt Andy bij VDL Nedcar in Born, waar hij meewerkt aan nieuwe projecten voor het moderniseren van de fabriek.

Pin It on Pinterest