Progressive Web Apps (PWA)

PWAs with Next.js

20 min Lesson 22 of 30

Building PWAs with Next.js

Next.js provides excellent support for Progressive Web Apps through the next-pwa plugin. This lesson covers how to transform your Next.js application into a full-featured PWA with offline support and app-like behavior.

Installing next-pwa

The next-pwa plugin is the most popular and maintained solution for adding PWA capabilities to Next.js applications.

# Install next-pwa npm install next-pwa # Or with Yarn yarn add next-pwa # Directory structure: pages/ _app.js _document.js index.js public/ manifest.json icons/ next.config.js

Configuring next.config.js

Configure next-pwa in your Next.js configuration file with custom options for service worker generation.

// next.config.js const withPWA = require('next-pwa')({ dest: 'public', register: true, skipWaiting: true, disable: process.env.NODE_ENV === 'development', runtimeCaching: [ { urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i, handler: 'CacheFirst', options: { cacheName: 'google-fonts', expiration: { maxEntries: 4, maxAgeSeconds: 365 * 24 * 60 * 60 // 1 year } } }, { urlPattern: /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i, handler: 'StaleWhileRevalidate', options: { cacheName: 'static-font-assets', expiration: { maxEntries: 4, maxAgeSeconds: 7 * 24 * 60 * 60 // 7 days } } }, { urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i, handler: 'StaleWhileRevalidate', options: { cacheName: 'static-image-assets', expiration: { maxEntries: 64, maxAgeSeconds: 24 * 60 * 60 // 24 hours } } }, { urlPattern: /\.(?:js)$/i, handler: 'StaleWhileRevalidate', options: { cacheName: 'static-js-assets', expiration: { maxEntries: 32, maxAgeSeconds: 24 * 60 * 60 // 24 hours } } }, { urlPattern: /\.(?:css|less)$/i, handler: 'StaleWhileRevalidate', options: { cacheName: 'static-style-assets', expiration: { maxEntries: 32, maxAgeSeconds: 24 * 60 * 60 // 24 hours } } }, { urlPattern: /\/api\/.*/i, handler: 'NetworkFirst', method: 'GET', options: { cacheName: 'apis', expiration: { maxEntries: 16, maxAgeSeconds: 24 * 60 * 60 // 24 hours }, networkTimeoutSeconds: 10 } } ] }); module.exports = withPWA({ reactStrictMode: true, // Your other Next.js config options });
Pro Tip: Set disable: process.env.NODE_ENV === 'development' to prevent service worker registration during development, which can cause caching issues.

Creating the Manifest

Create a manifest.json file in the public directory to define your PWA metadata.

// public/manifest.json { "name": "My Next.js PWA", "short_name": "Next PWA", "description": "A Progressive Web App built with Next.js", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#000000", "orientation": "portrait-primary", "icons": [ { "src": "/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png", "purpose": "maskable any" }, { "src": "/icons/icon-96x96.png", "sizes": "96x96", "type": "image/png", "purpose": "maskable any" }, { "src": "/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png", "purpose": "maskable any" }, { "src": "/icons/icon-144x144.png", "sizes": "144x144", "type": "image/png", "purpose": "maskable any" }, { "src": "/icons/icon-152x152.png", "sizes": "152x152", "type": "image/png", "purpose": "maskable any" }, { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable any" }, { "src": "/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png", "purpose": "maskable any" }, { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable any" } ] }

Linking Manifest in _document.js

Add manifest and theme color meta tags to your custom Document component.

// pages/_document.js import { Html, Head, Main, NextScript } from 'next/document'; export default function Document() { return ( <Html lang="en"> <Head> <link rel="manifest" href="/manifest.json" /> <link rel="apple-touch-icon" href="/icons/icon-192x192.png" /> <meta name="theme-color" content="#000000" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-title" content="Next PWA" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); }

Offline Fallback Pages

Create custom offline fallback pages that display when users lose connectivity.

// pages/_offline.js export default function Offline() { return ( <div style={{ textAlign: 'center', padding: '50px' }}> <h1>You are currently offline</h1> <p>Please check your internet connection and try again.</p> <style jsx>{` div { min-height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; } `}</style> </div> ); }
// Update next.config.js to use offline fallback const withPWA = require('next-pwa')({ dest: 'public', register: true, skipWaiting: true, disable: process.env.NODE_ENV === 'development', fallbacks: { document: '/_offline', image: '/static/fallback-image.png' } });

Online/Offline Detection Hook

Create a custom hook to detect and respond to network status changes.

// hooks/useOnline.js import { useState, useEffect } from 'react'; export function useOnline() { const [isOnline, setIsOnline] = useState( typeof navigator !== 'undefined' ? navigator.onLine : true ); useEffect(() => { const handleOnline = () => setIsOnline(true); const handleOffline = () => setIsOnline(false); window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); return isOnline; } // Usage in component import { useOnline } from '../hooks/useOnline'; export default function Home() { const isOnline = useOnline(); return ( <div> {!isOnline && ( <div className="offline-banner"> You are offline. Some features may not work. </div> )} {/* Page content */} </div> ); }
Important: Next.js PWAs require building for production to test service worker functionality. Run npm run build && npm start to test PWA features locally.

Advanced PWA Configuration

Configure advanced options for better control over service worker behavior.

// next.config.js - Advanced configuration const withPWA = require('next-pwa')({ dest: 'public', register: true, skipWaiting: true, disable: process.env.NODE_ENV === 'development', scope: '/', sw: 'service-worker.js', publicExcludes: ['!robots.txt', '!sitemap.xml'], buildExcludes: [/middleware-manifest\.json$/], maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB cacheOnFrontEndNav: true, aggressiveFrontEndNavCaching: false, reloadOnOnline: true, swcMinify: true });
Exercise:
  1. Install next-pwa in a Next.js project
  2. Configure next.config.js with runtime caching strategies
  3. Create a manifest.json with proper icons
  4. Add manifest link to _document.js
  5. Create an offline fallback page
  6. Implement the useOnline hook
  7. Build and test PWA features in production mode
  8. Test offline functionality using Chrome DevTools
Performance Tip: Use the NetworkFirst strategy for API routes to ensure fresh data when online, but still provide cached responses when offline. Use CacheFirst for static assets that rarely change.