Back to blog

Progressive Web Apps with Next.js - Modern Web Capabilities

5 min readBy Mustafa Akkaya
#Next.js#PWA#Service Workers#Web Development

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:

script.sh

npm install next-pwa

Configure your Next.js app:

script.js

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:

data.json

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:

app.ts

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:

script.js

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:

app.ts

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:

app.ts

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

app.ts

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

app.ts

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

app.ts

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.