Progressive Web Apps with Next.js - Modern Web Capabilities
Progressive Web Apps (PWA) combine the best of web and mobile applications. With Next.js, you can easily add PWA capabilities to deliver fast, reliable, and engaging experiences. Let's build a modern PWA.
Why Build a PWA?
PWAs offer significant advantages:
- Offline Functionality - Work without internet connection
- Fast Loading - Cached assets load instantly
- Installable - Add to home screen like native apps
- Push Notifications - Re-engage users effectively
- Responsive - Work on any device and screen size
- SEO Friendly - Discoverable by search engines
Setting Up PWA in Next.js
Install the necessary package:
npm install next-pwa
Configure your Next.js app:
class="keyword">class="comment">// next.config.js
class="keyword">class="keyword">const withPWA = require(class="keyword">class="string">"next-pwa")({
dest: class="keyword">class="string">"class="keyword">public",
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === class="keyword">class="string">"development",
});
module.exports = withPWA({
reactStrictMode: true,
});
Web App Manifest
Create a manifest file to define your app's appearance:
class="keyword">class="comment">// class="keyword">public/manifest.json
{
class="keyword">class="string">"name": class="keyword">class="string">"Your App Name",
class="keyword">class="string">"short_name": class="keyword">class="string">"App",
class="keyword">class="string">"description": class="keyword">class="string">"Your app description",
class="keyword">class="string">"start_url": class="keyword">class="string">"/",
class="keyword">class="string">"display": class="keyword">class="string">"standalone",
class="keyword">class="string">"background_color": class="keyword">class="string">"#ffffff",
class="keyword">class="string">"theme_color": class="keyword">class="string">"#000000",
class="keyword">class="string">"icons": [
{
class="keyword">class="string">"src": class="keyword">class="string">"/icons/icon-192x192.png",
class="keyword">class="string">"sizes": class="keyword">class="string">"192x192",
class="keyword">class="string">"class="keyword">type": class="keyword">class="string">"image/png"
},
{
class="keyword">class="string">"src": class="keyword">class="string">"/icons/icon-512x512.png",
class="keyword">class="string">"sizes": class="keyword">class="string">"512x512",
class="keyword">class="string">"class="keyword">type": class="keyword">class="string">"image/png"
}
]
}
Link the manifest in your app:
class="keyword">class="comment">// app/layout.tsx
class="keyword">export class="keyword">class="keyword">const metadata = {
manifest: class="keyword">class="string">"/manifest.json",
themeColor: class="keyword">class="string">"#000000",
appleWebApp: {
capable: true,
statusBarStyle: class="keyword">class="string">"class="keyword">default",
title: class="keyword">class="string">"Your App Name",
},
};
Implementing Offline Support
Service workers handle offline functionality. With next-pwa, they're generated automatically. Create a custom service worker for advanced features:
class="keyword">class="comment">// class="keyword">public/sw.js
self.addEventListener(class="keyword">class="string">"fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
class="keyword">class="keyword">return response || fetch(event.request);
})
);
});
class="keyword">class="comment">// Cache API responses
class="keyword">class="keyword">const CACHE_NAME = class="keyword">class="string">"api-cache-v1";
class="keyword">class="keyword">const API_URLS = [class="keyword">class="string">"/api/data", class="keyword">class="string">"/api/posts"];
self.addEventListener(class="keyword">class="string">"install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
class="keyword">class="keyword">return cache.addAll(API_URLS);
})
);
});
Adding Install Prompt
Create a component to prompt users to install:
class="keyword">class="comment">// components/InstallPrompt.tsx
class="keyword">class="string">"use client";
class="keyword">import { useState, useEffect } class="keyword">from class="keyword">class="string">"react";
class="keyword">export class="keyword">default class="keyword">class="keyword">function InstallPrompt() {
class="keyword">class="keyword">const [deferredPrompt, setDeferredPrompt] = useState(class="keyword">null);
class="keyword">class="keyword">const [showPrompt, setShowPrompt] = useState(false);
useEffect(() => {
class="keyword">class="keyword">const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
setShowPrompt(true);
};
window.addEventListener(class="keyword">class="string">"beforeinstallprompt", handler);
class="keyword">class="keyword">return () => {
window.removeEventListener(class="keyword">class="string">"beforeinstallprompt", handler);
};
}, []);
class="keyword">class="keyword">const handleInstall = class="keyword">class="keyword">async () => {
class="keyword">class="keyword">if (!deferredPrompt) class="keyword">class="keyword">return;
deferredPrompt.prompt();
class="keyword">class="keyword">const { outcome } = class="keyword">class="keyword">await deferredPrompt.userChoice;
class="keyword">class="keyword">if (outcome === class="keyword">class="string">"accepted") {
setShowPrompt(false);
}
setDeferredPrompt(class="keyword">null);
};
class="keyword">class="keyword">if (!showPrompt) class="keyword">class="keyword">return class="keyword">null;
class="keyword">class="keyword">return (
class="keyword">class="string">"fixed bottom-4 right-4 p-4 bg-white shadow-lg rounded-lg">
class="keyword">class="string">"font-bold mb-2">Install App
class="keyword">class="string">"text-sm mb-4">Install class="keyword">class="keyword">for a better experience
class="keyword">class="string">"flex gap-2">
);
}
Caching Strategies
Different content needs different caching approaches:
class="keyword">class="comment">// lib/cache-strategies.ts
class="keyword">export class="keyword">class="keyword">const cacheStrategies = {
class="keyword">class="comment">// Static assets: Cache first
class="keyword">static: {
cacheName: class="keyword">class="string">"class="keyword">static-assets",
strategy: class="keyword">class="string">"CacheFirst",
},
class="keyword">class="comment">// API calls: Network first, fallback to cache
api: {
cacheName: class="keyword">class="string">"api-cache",
strategy: class="keyword">class="string">"NetworkFirst",
networkTimeoutSeconds: 3,
},
class="keyword">class="comment">// Images: Stale class="keyword">class="keyword">while revalidate
images: {
cacheName: class="keyword">class="string">"images",
strategy: class="keyword">class="string">"StaleWhileRevalidate",
maxEntries: 50,
maxAgeSeconds: 30 * 24 * 60 * 60, class="keyword">class="comment">// 30 days
},
};
Testing Your PWA
Use Chrome DevTools to test PWA features:
1. Open DevTools → Application tab
2. Check "Service Workers" for registration
3. Test "Offline" mode under Network tab
4. Validate manifest in "Manifest" section
5. Run Lighthouse audit for PWA score
Best Practices
1. Optimize Cache Size
class="keyword">class="comment">// Keep caches manageable
class="keyword">class="keyword">const MAX_CACHE_SIZE = 50;
class="keyword">class="keyword">const CACHE_EXPIRY_DAYS = 7;
2. Handle Updates Gracefully
class="keyword">class="comment">// Notify users of updates
class="keyword">class="keyword">if (class="keyword">class="string">"serviceWorker" in navigator) {
navigator.serviceWorker.register(class="keyword">class="string">"/sw.js").then((reg) => {
reg.addEventListener(class="keyword">class="string">"updatefound", () => {
class="keyword">class="keyword">const newWorker = reg.installing;
newWorker?.addEventListener(class="keyword">class="string">"statechange", () => {
class="keyword">class="keyword">if (newWorker.state === class="keyword">class="string">"installed") {
class="keyword">class="comment">// Show update notification
}
});
});
});
}
3. Provide Offline Feedback
class="keyword">class="comment">// components/OfflineIndicator.tsx
class="keyword">class="string">"use client";
class="keyword">import { useState, useEffect } class="keyword">from class="keyword">class="string">"react";
class="keyword">export class="keyword">default class="keyword">class="keyword">function OfflineIndicator() {
class="keyword">class="keyword">const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
setIsOnline(navigator.onLine);
class="keyword">class="keyword">const handleOnline = () => setIsOnline(true);
class="keyword">class="keyword">const handleOffline = () => setIsOnline(false);
window.addEventListener(class="keyword">class="string">"online", handleOnline);
window.addEventListener(class="keyword">class="string">"offline", handleOffline);
class="keyword">class="keyword">return () => {
window.removeEventListener(class="keyword">class="string">"online", handleOnline);
window.removeEventListener(class="keyword">class="string">"offline", handleOffline);
};
}, []);
class="keyword">class="keyword">if (isOnline) class="keyword">class="keyword">return class="keyword">null;
class="keyword">class="keyword">return (
class="keyword">class="string">"fixed top-0 w-full bg-yellow-500 text-center p-2">
You are currently offline
);
}
Production Checklist
Before deploying your PWA:
- [ ] Valid manifest.json with all required fields
- [ ] Icons in multiple sizes (192x192, 512x512)
- [ ] Service worker properly registered
- [ ] HTTPS enabled (required for PWA)
- [ ] Offline page implemented
- [ ] Cache strategy configured
- [ ] Install prompt tested
- [ ] Lighthouse PWA score > 90
Conclusion
PWAs bridge the gap between web and native apps. With Next.js and modern web APIs, you can deliver fast, reliable experiences that work offline and feel native. Start with basic offline support, then gradually add features like push notifications and background sync.
The web platform continues to evolve, and PWAs represent the future of accessible, performant web applications that work everywhere.