A Progressive Web App, or PWA, is still a website: users can open it from a URL and search engines can discover its public pages. The difference is that the browser can install it, launch it in a standalone window and let a service worker support offline or resilient behavior. We build PWAs by treating installation, caching and updates as parts of the product architecture—not as a plugin switched on at the end.

How does a Progressive Web App work?

The visible application is ordinary HTML, CSS and JavaScript. React can render the interface in the browser, while Next.js can combine server-rendered routes, React Server Components and client-side interaction. A web app manifest supplies install metadata such as the app name, icons, start URL, theme color and standalone display mode. HTTPS protects the connection and unlocks browser capabilities that require a secure context.

A service worker is a separate JavaScript worker registered by the page. After installation and activation, it can sit between the application and the network, respond from a cache, fetch fresh data or combine both strategies. It cannot access the DOM directly. The page, service worker, Cache Storage and optional IndexedDB database are separate layers that communicate through events and messages.

The PWA architecture and service worker lifecycle

On the first visit, the browser downloads the page normally. The application registers the service worker, which triggers install and activate events. During installation it can precache a small application shell. During activation it can remove obsolete caches. Once the worker controls a page, fetch events let it decide whether each request should use the network, a cached response or a fallback.

Updates are intentionally cautious. A newly downloaded worker usually waits while an older version controls open tabs, preventing two incompatible versions from running at once. We design an explicit update experience: notify the user, allow them to finish important work and reload when the new worker is ready. Forcing activation without coordinating application state can lose unsaved input.

How we build a React PWA with Vite

For a React and Vite application, we usually generate the manifest and production service worker with vite-plugin-pwa, which uses Workbox for precaching and runtime caching. Install the package as a development dependency, add VitePWA to the plugin list and define the manifest in vite.config.ts. The icon files must exist at the declared URLs and include suitable 192 × 192 and 512 × 512 PNG variants.

The configuration below creates an installable React PWA and asks the application to prompt before applying a service worker update. The navigation fallback is appropriate for a client-side React Router application whose server is configured to serve the app shell for valid routes. Multi-page or server-rendered projects need a routing and caching strategy that matches their actual HTML responses.

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'prompt',
      manifest: {
        name: 'My React PWA',
        short_name: 'MyPWA',
        start_url: '/',
        display: 'standalone',
        background_color: '#ffffff',
        theme_color: '#006b63',
        icons: [
          { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icon-512.png', sizes: '512x512', type: 'image/png' }
        ]
      },
      workbox: { navigateFallback: '/index.html' }
    })
  ]
})

Register the service worker in React

A generated registration helper can manage updates for you, but understanding the native API is valuable. Registration should run only in the browser and normally after the page loads. The service worker file must be served from the scope it needs to control: a worker at /sw.js can control the origin, while one inside /assets/ has a narrower default scope.

Treat registration failure as a recoverable enhancement failure. The online application should continue working when service workers are unsupported or blocked. Log failures during development, expose update state to the interface and test the installed production build rather than assuming that successful registration means the offline experience is correct.

src/main.tsx
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js')
      console.info('Service worker scope:', registration.scope)
    } catch (error) {
      console.error('Service worker registration failed', error)
    }
  })
}

Create a Next.js PWA manifest with the App Router

Next.js App Router has built-in support for a typed manifest metadata file. Create app/manifest.ts and return a MetadataRoute.Manifest object. Next.js exposes it as the web app manifest, so the name, description, icons and display behavior stay in application code. Static app/manifest.json is also supported when the values never change.

Keep start_url inside the deployment scope and make the icon purpose deliberate. A maskable icon includes safe padding so operating systems can crop it into different shapes. Theme and background colors should match the first painted interface to avoid a jarring transition when the installed app launches.

app/manifest.ts
import type { MetadataRoute } from 'next'

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: 'My Next.js PWA',
    short_name: 'NextPWA',
    description: 'An installable Progressive Web App built with Next.js',
    start_url: '/',
    display: 'standalone',
    background_color: '#ffffff',
    theme_color: '#006b63',
    icons: [
      { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
      { src: '/icon-512.png', sizes: '512x512', type: 'image/png' }
    ]
  }
}

Add a service worker and caching to Next.js

A simple Next.js service worker can live at public/sw.js and be registered by a small client component. For production offline support, we prefer a maintained service-worker tool such as Workbox or a Next.js integration such as Serwist because it can generate a correct precache list for hashed assets. The cache policy must still be designed by the team; generation does not decide which business data is safe to store.

Use cache-first for versioned static assets, stale-while-revalidate for content that can tolerate brief staleness and network-first for navigations that should be fresh but need an offline fallback. Do not broadly cache authenticated pages, API responses, payment data or personalized HTML. Next.js server output can vary by cookies, headers and revalidation rules, so a service worker must not flatten those distinctions.

public/sw.js
const CACHE = 'app-shell-v1'
const APP_SHELL = ['/', '/offline', '/icon-192.png']

self.addEventListener('install', event => {
  event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(APP_SHELL)))
})

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.filter(key => key !== CACHE).map(key => caches.delete(key))
    ))
  )
})

self.addEventListener('fetch', event => {
  if (event.request.mode !== 'navigate') return
  event.respondWith(
    fetch(event.request).catch(() => caches.match('/offline'))
  )
})

Design offline data, installation and updates

Offline UI and offline data are different problems. Cache Storage is suitable for request and response objects such as scripts, styles, images and selected pages. IndexedDB is better for structured drafts, an outbox and records that must be synchronized later. If users can edit while offline, define conflict rules, visible sync status and retry behavior before building the interface.

Installation support varies by browser and platform, so never hide core functionality behind an install button. Detect standalone display mode, provide clear platform-specific instructions when needed and treat the browser’s install event as an enhancement. When an update is available, explain what will happen and reload only after the user accepts or reaches a safe point.

PWA security and performance rules

Serve the production PWA over HTTPS and keep the service worker small. Restrict caching to same-origin GET requests unless a reviewed use case requires more. Never place secrets in the manifest, client bundle or service worker. Validate push-message data, require permission from a clear user action and avoid requesting notifications on the first page view.

Precache only the assets needed to start. An oversized precache slows the first installation, consumes storage and increases the chance that one failed request breaks the install event. Apply cache limits and expiration rules to runtime images or documents. Measure start time on a real mobile connection, including the first uncached visit—the PWA must remain a fast website before it becomes an installed app.

Test and deploy a React or Next.js PWA

Test a production build on HTTPS or localhost. In browser developer tools, inspect the manifest, icon rendering, service worker status and Cache Storage. Verify a clean first load, repeat load, offline launch, slow network, expired cache and an upgrade from the previous deployed worker. Test direct visits to nested routes because a cached homepage can hide broken server routing.

Run accessibility and performance audits, but do not treat one Lighthouse score as the release decision. Install the PWA on Android, iOS and at least one desktop platform used by the audience. Confirm keyboard navigation, safe areas, theme colors, orientation, share behavior and recovery after storage is cleared. Monitor service-worker errors and version adoption after deployment.

React PWA or Next.js PWA: which should you choose?

Choose React with Vite when the product is primarily a client-side application, static hosting is valuable and the team wants direct control over routing and API boundaries. Choose Next.js when public routes benefit from server rendering, metadata, route-level data loading or server functions. Both can produce an excellent installable PWA; the correct choice depends on rendering, data and deployment requirements rather than the manifest format.

Before release, confirm the manifest and icons, HTTPS, service-worker scope, offline fallback, cache versioning, update prompt, storage limits, privacy rules and real-device installation. A successful Progressive Web App combines a dependable website with carefully chosen app capabilities. React or Next.js supplies the application structure; the manifest, service worker and product decisions create the PWA experience.

Explore more practical guidance in the development archive, or see the technology stack TERNYXA uses to design and engineer production systems.