PWA Development
Progressive Web App Setup
Transform your web application into a native app-like experience with offline support and installation
75 min
Duration
Intermediate
Level
4
Steps
PWA Benefits
Progressive Web Apps combine the best of web and mobile apps
App-like Experience
Native app feel with web technologies
- Home screen installation
- Fullscreen experience
- Native-like navigation
- Platform integration
Offline Functionality
Work seamlessly without internet connection
- Cached resources
- Offline data access
- Background sync
- Fallback pages
Push Notifications
Engage users with timely notifications
- Real-time updates
- User engagement
- Background messaging
- Cross-platform delivery
Performance Benefits
Fast loading and smooth interactions
- Instant loading
- Reduced bandwidth
- Improved UX
- Better conversion rates
Implementation Guide
1
Create Web App Manifest
Define your PWA metadata and appearance
// public/manifest.json
{
"name": "AEROSNAP Application",
"short_name": "AEROSNAP",
"description": "Modern web application built with AEROSNAP design system",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3B82F6",
"orientation": "portrait-primary",
"categories": ["business", "productivity"],
"lang": "en-US",
"dir": "ltr",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
}
],
"screenshots": [
{
"src": "/screenshots/desktop-1.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "/screenshots/mobile-1.png",
"sizes": "640x1136",
"type": "image/png",
"form_factor": "narrow"
}
]
}2
Service Worker Setup
Implement caching strategies and offline support
// public/sw.js
const CACHE_NAME = 'aerosnap-v1';
const STATIC_CACHE = 'static-v1';
const DYNAMIC_CACHE = 'dynamic-v1';
const STATIC_ASSETS = [
'/',
'/offline',
'/manifest.json',
'/icons/icon-192x192.png',
'/icons/icon-512x512.png'
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
return cache.addAll(STATIC_ASSETS);
})
);
self.skipWaiting();
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
return caches.delete(cacheName);
}
})
);
})
);
self.clients.claim();
});
// Fetch event - implement caching strategies
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Handle navigation requests
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then((response) => {
// Clone response for caching
const responseClone = response.clone();
caches.open(DYNAMIC_CACHE).then((cache) => {
cache.put(request, responseClone);
});
return response;
})
.catch(() => {
// Serve from cache or offline page
return caches.match(request).then((cached) => {
return cached || caches.match('/offline');
});
})
);
return;
}
// Handle API requests
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const responseClone = response.clone();
caches.open(DYNAMIC_CACHE).then((cache) => {
cache.put(request, responseClone);
});
}
return response;
})
.catch(() => {
return caches.match(request);
})
);
return;
}
// Handle static assets
event.respondWith(
caches.match(request).then((cached) => {
return cached || fetch(request).then((response) => {
const responseClone = response.clone();
caches.open(STATIC_CACHE).then((cache) => {
cache.put(request, responseClone);
});
return response;
});
})
);
});
// Background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
// Push notifications
self.addEventListener('push', (event) => {
const options = {
body: event.data ? event.data.text() : 'New update available',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'View',
icon: '/icons/checkmark.png'
},
{
action: 'close',
title: 'Close',
icon: '/icons/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification('AEROSNAP Update', options)
);
});3
Next.js PWA Integration
Configure Next.js for PWA support
// next.config.js
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development'
});
module.exports = withPWA({
// Your existing Next.js config
reactStrictMode: true,
swcMinify: true,
experimental: {
appDir: true,
},
// PWA-specific configuration
pwa: {
dest: 'public',
register: true,
skipWaiting: true,
sw: 'sw.js',
disable: process.env.NODE_ENV === 'development'
}
});
// app/layout.tsx - Add manifest link
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#3B82F6" />
<meta name="mobile-web-app-capable" content="yes" />
<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="AEROSNAP" />
<link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
</head>
<body>{children}</body>
</html>
);
}4
Install Prompt & Notifications
Handle PWA installation and push notifications
// hooks/usePWA.ts
'use client';
import { useEffect, useState } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export function usePWA() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstallable, setIsInstallable] = useState(false);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
// Check if already installed
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
setIsInstalled(isStandalone);
// Listen for install prompt
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
setIsInstallable(true);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
// Listen for successful installation
window.addEventListener('appinstalled', () => {
setIsInstalled(true);
setIsInstallable(false);
setDeferredPrompt(null);
});
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
};
}, []);
const installPWA = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setDeferredPrompt(null);
setIsInstallable(false);
}
};
const requestNotificationPermission = async () => {
if (!('Notification' in window)) return false;
const permission = await Notification.requestPermission();
return permission === 'granted';
};
return {
isInstallable,
isInstalled,
installPWA,
requestNotificationPermission
};
}
// components/PWAPrompt.tsx
import { usePWA } from '@/hooks/usePWA';
import { Download, X } from 'lucide-react';
export function PWAPrompt() {
const { isInstallable, installPWA } = usePWA();
const [showPrompt, setShowPrompt] = useState(true);
if (!isInstallable || !showPrompt) return null;
return (
<div className="fixed bottom-4 left-4 right-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Download className="w-6 h-6 text-blue-600" />
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">
Install AEROSNAP
</h3>
<p className="text-sm text-gray-600 dark:text-gray-300">
Get the full app experience
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={installPWA}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Install
</button>
<button
onClick={() => setShowPrompt(false)}
className="p-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
}Best Practices
Performance Optimization
- Implement efficient caching strategies
- Optimize bundle sizes for faster loading
- Use lazy loading for non-critical resources
- Monitor Core Web Vitals metrics
User Experience
- Provide clear offline indicators
- Design intuitive installation prompts
- Ensure smooth app-like navigation
- Handle network connectivity changes
Security & Privacy
- Use HTTPS for all PWA features
- Implement proper permission requests
- Secure sensitive data in cache
- Follow privacy best practices