Progressive Web Apps: Building Offline-First Apps with Superpowers

Unlock your web app's true potential. Learn how to transform any website into a powerful, offline-first Progressive Web App (PWA), from plain HTML to modern frameworks like React and Vite.


PWA 2.0: Building Offline-First Apps with Superpowers

What is a PWA?

For the veterans out there, PWA stands for Progressive Web Apps. But if you are a newbie just like me, you can think of it as “web apps on steroids”.

As a formal definition, A progressive web app (PWA) is an app that's built using web platform technologies, but that provides a user experience like that of a platform-specific app. — MDN Web Docs

Why are PWAs Different?

A PWA is basically a webapp, that has some added features that a normal webapp does not have, like:

  • Notifications Support.
  • Ability to run in backgrounds.
  • Caching support even when the app is closed.
  • Multi-Platform support on a single codebase.

It basically allows you to go ahead and release your webapp, as an app, without using any native languages, like Kotlin, Swift or Objective-C. It just takes the webapp you developed, using any technology, from the basic HTML, CSS and JS page or any fancy frameworks like NextJS, React JS or Vue JS.

Now if this hooked you up, and you want to try to make your webapp a PWA, go ahead and open up your code editors.

This article covers how to make PWAs in the following technologies:

  • Plain HTML, CSS and JS
  • Vite + React

Requirements to Convert Your Web App to PWA.

PWAs are really simple to develop, with a minimal setup. You only need two things

  • A Web App Manifest
  • A Service Worker

Let’s go ahead and dive deep into what are they and why they are used.

Why is a Web Manifest Used?

The “Web Manifest” is basically a configuration file that tells the browser, the name of your app, the logo of your app, the splash screens and so on, when someone downloads your PWA.

It is a JSON document, defined by the Web Application Manifest format.

Some common used keys of the Web Manifest are:

Now among, this only 6 keys are mandatory for a PWA, while rest can be supplied as optional fields

  • name or short_name
  • icons must contain a 192px and 512px icon
  • start_url
  • display and/or display_override
  • prefer_related_applications must be false or not present

Why is a Service Worker Used?

“Service Worker” is basically a background script that runs separately from your web page and gives your PWA powerful capabilities like offline access, caching, and push notifications.

It is a JavaScript file, defined by the Service Worker API.

Some common used events of the Service Worker are:

  • install — Triggered when the service worker is first installed.
  • activate — Runs after installation to clean up old caches.
  • fetch — Intercepts network requests and serves responses (from cache or network).
  • message — Handles messages sent between pages and the service worker.
  • push — Fired when a push notification is received.
  • sync — Allows background synchronization when the device is online again.

Now among this, only registration of a service worker is mandatory for a PWA, while the rest can be supplied as optional features:

  • navigator.serviceWorker.register('/sw.js') must be present to enable a service worker.

Enough talks, and buildup. Now let’s start the action.

PWA Implementation

For Plain HTML, CSS And JS

To convert your website to a PWA, is not so hard as you think. Let me walk you through.

  • Step 1: Create a Web App Manifest File

    After knowing all about the Web Manifest, let us get that into action. Go ahead and create a manifest.json file in you root directory along with index.html and fill in this sample content.

    {
      "name": "My Awesome PWA",
      "short_name": "AwesomePWA",
      "description": "My first PWA with plain HTML.",
      "start_url": "/index.html",
      "display": "standalone",
      "background_color": "#ffffff",
      "theme_color": "#000000",
      "icons": [
        {
          "src": "images/icon-192x192.png",
          "sizes": "192x192",
          "type": "image/png"
        },
        {
          "src": "images/icon-512x512.png",
          "sizes": "512x512",
          "type": "image/png"
        }
      ]
    }
    • name: The full name of your app. This appears on the splash screen and app store.
    • short_name: A shorter name to be used on the user's home screen, like under an icon.
    • start_url: The page that loads when a user opens the PWA. Using . or / is a common practice to load the root directory.
    • display: How your app should be displayed. standalone makes it look and feel like a native app, without a browser's address bar. Other options include fullscreenminimal-ui, and browser.
    • icons: An array of objects defining your app's icons in various sizes. The browser uses these to display the app on the home screen, splash screen, and notifications. You must create these icon files and place them in the specified path (e.g., images/).
  • Step 2: Link the Manifest to Your HTML

    Now, you need to tell your HTML page where to find the manifest file. In the <head> section of your index.html file, add the following <link> tag:

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

    This tag creates the connection. Once a user visits your site, the browser will fetch this file and understand that your website has PWA capabilities.

  • Step 3: Create and Register a Service Worker

    The service worker is a JavaScript file that runs in the background, separate from your main web page. Its primary job is to act as a programmable network proxy, intercepting network requests and allowing you to control how your app handles caching and offline content.

    First, create a new file named sw.js (for "service worker") in the root directory of your project. You can give it any name, we just need to make connections into our index.html

    const CACHE_NAME = 'my-pwa-cache-v1';
    const urlsToCache = [
      '/',
      '/index.html',
      '/styles.css',
      '/scripts.js',
      '/images/icon-192x192.png',
      '/images/icon-512x512.png'
    ];
     
    self.addEventListener('install', (event) => {
      event.waitUntil(
        caches.open(CACHE_NAME)
          .then((cache) => {
            console.log('Opened cache');
            return cache.addAll(urlsToCache);
          })
      );
    });
     
    self.addEventListener('fetch', (event) => {
      event.respondWith(
        caches.match(event.request)
          .then((response) => {
            if (response) {
              return response;
            }
            return fetch(event.request);
          })
      );
    });
    • install event: This event is fired when the service worker is installed. Inside the event listener, we open a cache named my-pwa-cache-v1 and add all the files listed in urlsToCache to it. This ensures that these key assets are available for offline use.
    • fetch event: This event is fired every time the browser tries to fetch a resource (like an image, CSS file, or another HTML page). The code first checks if the requested resource exists in the cache. If it does, it serves the cached version, making the app faster and available offline. If not, it fetches the resource from the network.
  • Step 4: Register the Service Worker

    Now, you need to tell your website to use this service worker. In a separate JavaScript file (e.g., scripts.js or directly in a <script> tag in your index.html), add the following code:

    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
          .then((registration) => {
            console.log('Service Worker registered! Scope:', registration.scope);
          })
          .catch((err) => {
            console.log('Service Worker registration failed:', err);
          });
      });
    }

    This script checks if the browser supports service workers. If it does, it registers sw.js. The registration process defines the scope, which determines which files the service worker can control. By placing sw.js in the root, its scope is the entire website.

  • Step 5: Host Your Site Over HTTPS

    This is a critical requirement. A PWA must be served over a secure connection (HTTPS). This is because service workers have significant power (like intercepting requests), and browsers require this security to protect users from malicious attacks. During development, you can use localhost, which is considered a secure context.

  • Step 6: Test Your PWA

    To test your PWA, you can use the Lighthouse tool, which is built into Google Chrome's DevTools. Open your website, go to DevTools (F12), select the "Lighthouse" tab, and run an audit for "Progressive Web App." Lighthouse will provide a report, telling you what you've done right and what you need to fix to meet PWA standards.

For Vite + React App

Let’s say you already have developed a react app using Vite as your build tool. For a Vite + React app, the process of developing a PWA is significantly different from a plain HTML site because you can leverage a build tool plugin to handle most of the heavy lifting for you. Instead of manually creating and managing the manifest and service worker files, you'll use a package that automates this process. The most popular choice for Vite is vite-plugin-pwa.

Here is a detailed guide on how to add PWA functionality to your existing Vite + React application.

  • Step 1: Install the Vite PWA Plugin

    The first step is to add vite-plugin-pwa to your project as a development dependency. This is a crucial package that will handle the generation of the manifest file, the service worker, and the logic to register them.

    Open your terminal in the project's root directory and run the following command

    npm install vite-plugin-pwa -D

    If you use a different package manager, the command will be slightly different (e.g., yarn add vite-plugin-pwa -D).

  • Step 2: Configure the Plugin in vite.config.js

    This is where the magic happens. You'll import the plugin and add it to the plugins array in your vite.config.js (or vite.config.ts) file. You can then configure the PWA's properties directly in this file, which is much more convenient than a separate manifest file.

    Open your vite.config.js and update it as follows:

    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react';
    import { VitePWA } from 'vite-plugin-pwa';
     
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [
        react(),
        VitePWA({
          registerType: 'autoUpdate',
          includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
          manifest: {
            name: 'My React PWA',
            short_name: 'ReactPWA',
            description: 'My awesome React Progressive Web App!',
            theme_color: '#ffffff',
            icons: [
              {
                src: 'pwa-192x192.png',
                sizes: '192x192',
                type: 'image/png'
              },
              {
                src: 'pwa-512x512.png',
                sizes: '512x512',
                type: 'image/png'
              }
            ]
          }
        })
      ]
    });

    Here's a breakdown of the key configuration options:

    • registerType: 'autoUpdate': This tells the plugin to automatically register the service worker and update it whenever a new version of your app is deployed.
    • includeAssets: An array of assets that should be pre-cached. This is often used for favicons and other static files in your public folder.
    • manifest: This object is where you define the contents of your web app manifest. The plugin will automatically generate the manifest.json file based on these properties. This includes the name, short name, colors, and icons for your app. 🎨
  • Step 3: Add the PWA Icons

    Even with the plugin, you still need to provide the image files for your app's icons. The manifest configuration in the previous step references these files, so you must create them and place them in your public folder.

    Create two icons in a graphics editor:

    1. pwa-192x192.png (a 192x192 pixel icon)
    2. pwa-512x512.png (a 512x512 pixel icon)

    Place them in the public folder of your project. The vite-plugin-pwa plugin will automatically include and manage these assets.

  • Step 4: Add the PWA to index.html

    Unlike a plain HTML site where you manually link the manifest, vite-plugin-pwa handles this for you. However, you should ensure your index.html file has a <head> section with a proper <title> and a <meta name="theme-color"> tag to match your manifest.

    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name="theme-color" content="#ffffff" />
        <title>My React PWA</title>
      </head>
      <body>
        <div id="root"></div>
        <script type="module" src="/src/main.jsx"></script>
      </body>
    </html>

    The plugin will dynamically inject the necessary manifest link and service worker registration script during the build process.

  • Step 5: Test and Deploy

    Since Vite and vite-plugin-pwa are designed for modern web development, testing and deployment are straightforward.

    • Development: To test locally, simply run npm run dev. The plugin will use a mock service worker for development so you can debug offline functionality easily.
    • Production: To get the full PWA experience, you need to build your application and serve it.
      1. Run npm run build to create a production-ready build in the dist folder.
      2. Serve the dist folder using a static server. You can install a simple one for testing with npm install -g serve and then run serve -s dist. This will serve your PWA on localhost, which is a secure context.

Once you have your production build served over HTTPS, you can open it in a modern browser like Chrome. Look for the "install" button in the address bar to see if your PWA is ready to be installed! You can also use the Lighthouse tool in Chrome DevTools to audit your PWA and check if it meets all the criteria.

This approach is much more efficient than the manual process because the plugin handles file generation, caching strategies, and asset revisioning, ensuring your PWA is always up-to-date and reliable.

Final Words

That's it from this already long article. If this article get's enough views and helps you out, I might release another article soon for Next.JS and React + CRA cli.

I would love to hear from you. Let's connect on Instagram

Subscribe to Our Newsletter