Software Consulting Services

How to convert your web app into a Progressive Web Application (PWA)

Tags: Technologies

Progressive Web Application (PWA)

 

According to MDN, a Progressive Web Application is "an app that's built using web platform technologies, but that provides a user experience like that of a platform-specific app". It is very common to create web applications with responsive design, so that they scale visually with the user's screen, but PWAs allow us to take that one step further by providing installability and offline availability, among other features.

 

A PWA that is installable will appear as a standalone app in the user's device home screen after the user allows the installation. During installation, your app's essential source code is stored in the user's device, allowing them to use it even while offline.

 

Without PWAs, these features would normally be reserved for platform-specific (or native) apps only. Developing traditional platform-specific applications alongside a webapp can be costly, so bringing PWA features to an existing web application is often cheaper and faster, while still providing functionality that is very similar to a platform-specific app.

 

Converting your existing web app into a PWA can improve your end user's experience, especially on mobile, as long as you apply responsive design and accessibility best practices.

 

Limitations of PWAs

 

PWAs have some important limitations:

 

  • Since PWAs are still web apps that run through a browser, access to native functionality is very limited.
  • Fully native apps normally perform better than web apps (including PWAs) in terms of speed, memory and battery usage.

 

This is in contrast to technologies for building native platform-specific adds, eg. React Native. PWAs are best thought of as a simpler alternative for apps that can tolerate these limitations.

 

The manifest file

 

The first and most important addition to your codebase is the web app manifest file, a publicly available JSON file that describes your application. The manifest file contains simple key-value information about your app, including various properties that you would see in a typical app market.

Here is a sample manifest.json file:

 

{
  "name": "My PWA",
  "short_name": "mypwa",
  "description": "An example Progressive Web Application.",
  "orientation": "landscape",
  "categories": ["productivity", "utilities"],
  "start_url": "/",
  "scope": "/",
  "display": "fullscreen",
  "theme_color": "#1d301d",
  "background_color": "#1d301d",
  "icons": [
    {
      "src": "/favicon-512.png",
      "sizes": "512x512"
    },
    {
      "src": "/favicon-192.png",
      "sizes": "192x192"
    }
  ],
  "screenshots": [
    {
      "src": "screenshots/sample.webp",
      "sizes": "1280x720",
      "type": "image/webp"
    }
  ]
}

 

 

Generally, specifying more properties results in a better user experience. For example, some useful properties are:

 

  • theme_color: displays the given color on the top navigation bar on mobile.
  • screenshots: results in a friendlier installation prompt, and may also show up if the app is deployed to an app market.
  • categories: also useful in app markets.
  • icons: determines the image(s) displayed to represent your app in the user's home screen after installation.
  • scope and start_url: specifying values other than / is useful when not all of your website should be considered part of the PWA.

 

Installability requirements

 

According to MDN, as of April 2025, your app must contain a valid manifest.json file that includes at least the following properties, in order to be installable on Chromium-based browsers:

 

  • name or short_name
  • icons
  • start_url
  • display
  •  

Additionally, the prefer_related_applications experimental property must be absent or be set to false.

 

See the full reference for this here.

 

If all requirements are met, users will eventually be prompted to install your app. Some browsers may also require that the user interacts with the page, or that the user spends at least a certain amount of time using it, before prompting the user for installation.

 

Not all browsers support PWA installation. Notably, Firefox and Safari do not yet support it on desktop, even with a valid manifest.json. That being said, providing installability does not hinder user experience on browsers that do not support it. All major browsers still support bookmarking your web application.

 

Offline functionality with service workers

 

A service worker is a type of web worker. Workers are essentially Javascript-based tasks that execute separately from the webapp's main Javascript, and therefore have no access to the DOM, and other website-specific Javascript APIs.

 

For the purpose of enriching your PWA with offline functionality, you can provide a service worker that runs alongside our app. A service worker is a specific type of worker that can be used to intercept network requests, particularly to implement request caching (though this is not their only use case). Caching is useful for offline support, but if done correctly, it can also reduce network usage while online.

 

A service worker must be defined as a separate Javascript file, served as a public asset, and then registered via the main Javascript runtime. First, a service worker can be loaded and registered as such:

 

function registerServiceWorker() {
  // Note that not all browsers support service workers.
  // If this feature is unavailable, we skip adding the event listener altogether.
  if (!('serviceWorker' in navigator)) {
    return;
  }

  // we need to wait until the page loads fully before service worker registration is available
  window.addEventListener('load', async () => {
    try {
      // Make sure this javascript file is publicly accessible.
      // It is also possible to load multiple workers like this.
      await navigator.serviceWorker.register('/service-worker.js');

      console.info('Service worker has been registered.');
    } catch (error) {
      console.error('Error loading the service worker!', error);
    }
  });
}

// then, call it when needed
registerServiceWorker();

 

Then, inside service-worker.js, we can add any Javascript we'd like to run through the service worker. For example, a basic request intercept could look like this:

 

// service-worker.js

self.addEventListener('fetch', (event) => {
  console.log('Someone made a request to: ', event.request.url);
});

 

It is possible to implement sophisticated caching using only the fundamental APIs available to service workers. However, in this article, we'll make use of Workbox to simplify this process.

 

Important: As with various other web APIs, service workers will only run when served via HTTPS. All major browsers will refuse to run them while using HTTP, by default.

 

Using Workbox to streamline caching

 

Workbox is a free and open source library that implements multiple common service worker functionalities for you. Workbox provides a vast array of ready-to-use scripts called recipes, which are available via the workbox-recipes module. Furthermore, it also provides more general abstractions that you can put together yourself if you need to implement fine-tuned behavior via modules such as workbox-routing and workbox-strategies.

 

First, let's look at Workbox recipes. Here are some examples of simple Workbox recipe usage:

 

// service-worker.js

import {
  staticResourceCache,
  imageCache,
  googleFontsCache
} from 'workbox-recipes';
import { ExpirationPlugin } from 'workbox-expiration';
import { setDefaultHandler } from 'workbox-routing';

googleFontsCache();
staticResourceCache();
imageCache(); /* CacheFirst by default */

 

For many PWAs, simple recipe-based caching is often a ll that's needed to offer a proper offline experience. In this example, we make sure to cache Google fonts and static resources so that they will be available even while the user is offline. Internally, Workbox defines all required network request interceptors making sure to apply sensible defaults.

 

Next, let's look at caching strategies. Strategies dictate how exactly network requests are fulfilled, given that we could do so either with cache, or by actually using the network. In the previous example, the imageCache() recipe uses the CacheFirst strategy by default. This strategy prefers completing requests with the cache, and then if that's not possible, to fallback to the network. This strategy is useful for non-critical resources that are always safe to fetch from cache when possible.

 

In contrast, a strategy like NetworkFirst or NetworkOnly will prefer, or even require, that requests are fulfilled through the network. This is useful for . Another very useful strategy which is optimal for many use cases is StaleWhileRevalidate, which involves fetching resources from cache if available, while also updating them via the network for the next request.

 

See this guide for a full tour of Workbox caching strategies.

 

Finally, here is an example of a custom caching implementation with Workbox that allows the PWA to render an offline fallback page, using both a custom cache and a recipe:

 

// service-worker.js

import { warmStrategyCache, offlineFallback } from 'workbox-recipes';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { CacheFirst, NetworkOnly } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { setDefaultHandler } from 'workbox-routing';

const offlineCache = new CacheFirst({
  cacheName: 'offline-cache',
  plugins: [
    new CacheableResponsePlugin({
      statuses: [ 0, 200 ]
    }),
    new ExpirationPlugin({
      maxAgeSeconds: 86400 /* 1 day */
    })
  ]
});

warmStrategyCache({
  urls: [
    '/offline.html'
  ],
  strategy: offlineCache
});

setDefaultHandler(new NetworkOnly());

offlineFallback({
  pageFallback: '/offline.html'
});

 

Closing words

 

Converting your app into a Progressive Web App is relatively easy and cost effective, and is often all that is needed for apps without any native functionality requirements. You can get started by adding an app manifest file. Providing installability can allow your users an experience very similar to that of native apps. Finally, you can also implement sophisticated offline behavior using service workers, and Workbox to greatly simplify service worker development.