Create-nextjs-pwa

Friday 22/07/2022

·6 min read
Share:

A Progressive Web App is a regular web app with three additions: a web app manifest, a service worker, and HTTPS. The reward is "Add to Home Screen" on mobile, offline support, and the option of native-app-like permissions (push notifications, background sync). For Next.js, the next-pwa package handles 95% of the wiring — what's left is writing the manifest, dropping in icons, and being aware of the gotchas. Below: a complete PWA setup, deployed in 15 minutes.

What you actually need

  1. manifest.json — describes the app to the browser
  2. A service worker — caches assets for offline use
  3. Icons in /public/ — at minimum 192×192 and 512×512 PNGs
  4. HTTPS — required for service workers (localhost is exempt for dev)

Everything else (theme colors, splash screens, OS-specific tweaks) is polish.

Install next-pwa

pnpm add next-pwa

Then configure it in next.config.js:

const withPWA = require('next-pwa')({
    dest: 'public',
    register: true,
    skipWaiting: true,
    disable: process.env.NODE_ENV === 'development',
})

module.exports = withPWA({
    reactStrictMode: true,
})

Key options:

  • dest: 'public' — emit the generated sw.js and workbox-*.js into public/ so they're served at the site root.
  • register: true — auto-injects the service-worker registration code into your pages.
  • skipWaiting: true — new service workers activate immediately instead of waiting for all tabs to close. Trade-off: users get the new version faster, but mid-session updates can cause cache inconsistencies. For content sites it's usually fine.
  • disable: ... === 'development' — service workers + Next.js fast refresh + cache invalidation = pain. Disable in dev and only test PWA features in pnpm build && pnpm start.

Write the manifest

Create public/manifest.json:

{
    "name": "My App",
    "short_name": "MyApp",
    "description": "What your app does in one sentence.",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#000000",
    "orientation": "portrait-primary",
    "icons": [
        {
            "src": "/icons/icon-192.png",
            "sizes": "192x192",
            "type": "image/png",
            "purpose": "any maskable"
        },
        {
            "src": "/icons/icon-512.png",
            "sizes": "512x512",
            "type": "image/png",
            "purpose": "any maskable"
        }
    ]
}

Key fields:

  • name vs short_namename shows in install dialogs, short_name shows under the home-screen icon (12 chars max recommended).
  • display: standalone — hides the browser chrome when launched from home screen. Other options: fullscreen, minimal-ui, browser.
  • theme_color — colors the OS status bar on Android.
  • icons — at least 192×192 and 512×512. "purpose": "any maskable" lets Android crop the icon to its system shape (circle, rounded square, etc.). Maskable icons need a safe zone in the center 80% — see maskable.app to test.
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
    return (
        <Html lang="en">
            <Head>
                <link rel="manifest" href="/manifest.json" />
                <meta name="theme-color" content="#000000" />
                <link rel="apple-touch-icon" href="/icons/icon-192.png" />
            </Head>
            <body>
                <Main />
                <NextScript />
            </body>
        </Html>
    )
}

The apple-touch-icon link is iOS-specific — Safari ignores manifest.json icons and uses this instead.

Build and test

pnpm build && pnpm start

Open http://localhost:3000 in Chrome → DevTools → Application tab:

  • Manifest section should show your app details with no errors.
  • Service Workers should show one registered worker, status "activated and running."
  • Storage → Cache Storage should show one or more caches with your assets.

Test "Add to Home Screen":

  • Chrome desktop: the install icon () appears in the URL bar when criteria are met.
  • Chrome Android: visit the site, tap menu → "Install app."
  • iOS Safari: tap Share → "Add to Home Screen" (works without a service worker but uses the manifest).

Offline support

next-pwa uses Workbox under the hood with sensible defaults: static assets (JS, CSS, images) get cached on first request; subsequent visits work offline as long as you've visited each page once.

For finer control, add a runtimeCaching array in your next-pwa config:

const withPWA = require('next-pwa')({
    dest: 'public',
    runtimeCaching: [
        {
            urlPattern: /^https?.*/,
            handler: 'NetworkFirst',
            options: {
                cacheName: 'all-cache',
                networkTimeoutSeconds: 10,
            },
        },
    ],
})

Workbox strategies:

| Strategy | Behavior | |---|---| | CacheFirst | Check cache, network only on miss. Fastest but stale. | | NetworkFirst | Check network, fall back to cache. Fresh but slower. | | StaleWhileRevalidate | Serve cache immediately, refresh in background. Best UX for most assets. | | NetworkOnly | Bypass cache. Use for /api/ routes. |

Edge cases worth knowing

  • Service workers cache aggressively. During development, an old service worker can serve a months-old version of your app. Always test PWA changes in an Incognito window, or "Unregister" the worker from DevTools first.
  • HTTPS is mandatory for service workers in production. localhost is exempt for development. If you self-host, you need a TLS cert — Let's Encrypt + Caddy or Nginx is the standard setup.
  • iOS Safari is the limiting platform. It supports manifest + Add to Home Screen + a subset of service-worker features, but no push notifications (until iOS 16.4+) and stricter cache limits.
  • Updating an installed PWA can be tricky — users keep the cached version until the service worker fetches new files. With skipWaiting: true, this happens on next visit; without it, on next browser restart.
  • next-pwa doesn't generate a sw.js for the App Router. For App Router projects, use @ducanh2912/next-pwa (a maintained fork) or serwist.

FAQ

Do I need next-pwa for a Next.js PWA?

No — you can write a manifest and a service worker by hand. next-pwa wraps Workbox and Next.js's build pipeline so you don't have to, and it handles the most common gotchas (dev disabling, cache invalidation, etc.). It's the path of least resistance.

Why does my service worker not update?

By default, the browser checks for a new service worker every 24 hours. If your sw.js content has changed, it'll update on the next page load. Force an update via navigator.serviceWorker.getRegistration().then(r => r.update()), or just unregister via DevTools during development.

Can I have an offline-only page?

Yes — pre-cache an /offline route and configure a fallback in runtimeCaching. When the network is unreachable and the requested page isn't cached, the service worker serves /offline instead. Workbox's precacheAndRoute plus a NavigationRoute with a catch handler is the standard pattern.

Does PWA work with Next.js App Router?

The original next-pwa was Pages-Router-only. For App Router, use the @ducanh2912/next-pwa fork or serwist — both implement the same Workbox integration with App-Router-compatible build hooks.

Share:
VA

Vadim Alakhverdov

Software developer writing about JavaScript, web development, and developer tools.

Related Posts