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:
- Install next-pwa in a Next.js project
- Configure next.config.js with runtime caching strategies
- Create a manifest.json with proper icons
- Add manifest link to _document.js
- Create an offline fallback page
- Implement the useOnline hook
- Build and test PWA features in production mode
- 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.