Back to Insights
Tutorials

Building Progressive Web Apps (PWA) in 2026

Ahmed Raza Dec 22, 2025 11 min read

The Power of Progressive Web Apps

PWAs combine the best of web and native apps, offering offline functionality, push notifications, and installability—all through a web browser.

What Makes a PWA?

A Progressive Web App must meet these criteria:

Core Requirements:

  • HTTPS - Secure connection required
  • Service Worker - Enables offline functionality
  • Web App Manifest - Defines app metadata
  • Responsive Design - Works on all devices

Enhanced Features:

  • Installable - Add to home screen
  • App-like - Full-screen mode
  • Discoverable - Found via search engines
  • Re-engageable - Push notifications
  • Linkable - Shareable via URL

Benefits of PWAs

For Users:

  • Fast loading, even on slow networks
  • Works offline
  • Less storage than native apps
  • No app store required
  • Always up-to-date

For Businesses:

  • Single codebase for all platforms
  • Lower development costs
  • Easier to maintain
  • Better SEO
  • No app store fees

Setting Up a PWA

1. Start with a Manifest File

Create manifest.json:

{
  "name": "My Awesome PWA",
  "short_name": "MyPWA",
  "description": "An awesome progressive web app",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0066cc",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

2. Link Manifest in HTML

<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0066cc" />

Implementing Service Workers

Service workers are the heart of PWAs.

Basic Service Worker:

// sw.js
const CACHE_NAME = "my-pwa-v1";
const urlsToCache = [
  "/",
  "/styles/main.css",
  "/scripts/main.js",
  "/images/logo.png",
];

// Install event
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache)),
  );
});

// Fetch event
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches
      .match(event.request)
      .then((response) => response || fetch(event.request)),
  );
});

// Activate event
self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName);
          }
        }),
      );
    }),
  );
});

Register Service Worker:

// main.js
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("/sw.js")
    .then((reg) => console.log("SW registered", reg))
    .catch((err) => console.error("SW registration failed", err));
}

Caching Strategies

Choose the right strategy for each resource:

1. Cache First (Cache Falling Back to Network)

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches
      .match(event.request)
      .then((response) => response || fetch(event.request)),
  );
});

Best for: Static assets (CSS, JS, images)

2. Network First (Network Falling Back to Cache)

self.addEventListener("fetch", (event) => {
  event.respondWith(
    fetch(event.request).catch(() => caches.match(event.request)),
  );
});

Best for: Dynamic content, API calls

3. Stale While Revalidate

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.match(event.request).then((response) => {
        const fetchPromise = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        return response || fetchPromise;
      });
    }),
  );
});

Best for: Frequently updated content

4. Network Only

self.addEventListener("fetch", (event) => {
  event.respondWith(fetch(event.request));
});

Best for: Always-fresh data (analytics, logs)

5. Cache Only

self.addEventListener("fetch", (event) => {
  event.respondWith(caches.match(event.request));
});

Best for: App shell

Push Notifications

Re-engage users with timely notifications.

1. Request Permission:

Notification.requestPermission().then((permission) => {
  if (permission === "granted") {
    console.log("Notification permission granted");
  }
});

2. Subscribe to Push:

navigator.serviceWorker.ready.then((registration) => {
  registration.pushManager
    .subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(publicKey),
    })
    .then((subscription) => {
      // Send subscription to server
      fetch("/subscribe", {
        method: "POST",
        body: JSON.stringify(subscription),
        headers: {
          "Content-Type": "application/json",
        },
      });
    });
});

3. Handle Push Events:

// sw.js
self.addEventListener("push", (event) => {
  const data = event.data.json();

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: "/icons/icon-192x192.png",
      badge: "/icons/badge-72x72.png",
      data: {
        url: data.url,
      },
    }),
  );
});

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  event.waitUntil(clients.openWindow(event.notification.data.url));
});

App Shell Architecture

Separate UI shell from content.

Benefits:

  • Instant loading of UI
  • Content loads separately
  • Better perceived performance
  • Works offline

Implementation:

const SHELL_CACHE = "app-shell-v1";
const CONTENT_CACHE = "content-v1";

const shellAssets = ["/", "/styles/app.css", "/scripts/app.js", "/shell.html"];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(SHELL_CACHE).then((cache) => cache.addAll(shellAssets)),
  );
});

Making Your PWA Installable

Install Prompt:

let deferredPrompt;

window.addEventListener("beforeinstallprompt", (event) => {
  // Prevent automatic prompt
  event.preventDefault();
  deferredPrompt = event;

  // Show custom install button
  showInstallButton();
});

function installApp() {
  if (deferredPrompt) {
    deferredPrompt.prompt();
    deferredPrompt.userChoice.then((choice) => {
      if (choice.outcome === "accepted") {
        console.log("User installed the app");
      }
      deferredPrompt = null;
    });
  }
}

Offline Functionality

Provide a good offline experience.

Offline Page:

// sw.js
const OFFLINE_URL = "/offline.html";

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.add(OFFLINE_URL)),
  );
});

self.addEventListener("fetch", (event) => {
  if (event.request.mode === "navigate") {
    event.respondWith(
      fetch(event.request).catch(() => caches.match(OFFLINE_URL)),
    );
  }
});

Background Sync

Sync data when connection is restored.

// Register sync
navigator.serviceWorker.ready.then((registration) => {
  registration.sync.register("sync-posts");
});

// Handle sync event
self.addEventListener("sync", (event) => {
  if (event.tag === "sync-posts") {
    event.waitUntil(syncPosts());
  }
});

async function syncPosts() {
  const posts = await getPostsFromIndexedDB();
  return Promise.all(
    posts.map((post) =>
      fetch("/api/posts", {
        method: "POST",
        body: JSON.stringify(post),
      }),
    ),
  );
}

Testing Your PWA

Lighthouse Audit:

  1. Open Chrome DevTools
  2. Go to Lighthouse tab
  3. Run PWA audit
  4. Fix any issues

Key Metrics:

  • Performance score > 90
  • Accessibility score > 90
  • Best Practices score > 90
  • SEO score > 90
  • PWA score = 100

Test Checklist:

  • ✓ Installable
  • ✓ Works offline
  • ✓ Fast on 3G
  • ✓ HTTPS enabled
  • ✓ Responsive design
  • ✓ Cross-browser tested

Twitter Lite:

  • 70% increase in tweets sent
  • 65% increase in pages per session
  • 20% decrease in bounce rate

Pinterest:

  • 60% increase in core engagements
  • 44% increase in user-generated ad revenue
  • 40% increase in time spent

Starbucks:

  • 2x daily active users
  • Order completion nearly matches mobile app

Tools and Libraries

Workbox: Google’s PWA toolkit for easier service worker implementation.

import { precacheAndRoute } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { StaleWhileRevalidate } from "workbox-strategies";

// Precache static assets
precacheAndRoute(self.__WB_MANIFEST);

// Cache API responses
registerRoute(/\/api\//, new StaleWhileRevalidate());

Other Tools:

  • PWA Builder - Generate PWA assets
  • Lighthouse - Audit and testing
  • Workbox - Service worker library
  • PWACompat - iOS compatibility

Conclusion

PWAs offer a compelling alternative to native apps with lower costs and broader reach. Start simple with offline support and installability, then add advanced features like push notifications and background sync. Test thoroughly, measure impact, and iterate based on user feedback. The web is getting more powerful, and PWAs are leading the way.

Share This Insight

Related Insights

View All