Introduction
Progressive Web Apps (PWAs) give you mobile app capabilities without the app store. Users can install them on their home screen, work offline, and receive push notifications. For startups, PWAs offer a fast path to mobile users without the overhead of native apps.
Why PWA?
Benefits
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PWA Benefits โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โ No app store approval needed โ
โ โ Instant updates (no App Store review) โ
โ โ Installable on home screen โ
โ โ Works offline โ
โ โ Push notifications โ
โ โ Native-like experience โ
โ โ Lower development cost โ
โ โ Single codebase for web + mobile โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
PWA vs Native App
# Comparison
pwa:
development: "Single web codebase"
distribution: "Web (no store)"
updates: "Instant"
cost: "One team"
offline: "Yes (service workers)"
push_notifications: "Yes"
app_store: "No"
native:
development: "iOS + Android = 2 teams"
distribution: "App Store / Play Store"
updates: "App Store review (days)"
cost: "2-3x more"
offline: "Yes"
push_notifications: "Yes"
app_store: "Yes"
Building a PWA
Web App Manifest
// public/manifest.json
{
"name": "My Startup App",
"short_name": "Startup",
"description": "The best startup app",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
// app/layout.tsx - Add manifest link
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#000000" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
</head>
<body>{children}</body>
</html>
)
}
Service Worker
// public/sw.ts (Next.js 13+)
/// <reference lib="webworker" />
const CACHE_NAME = 'my-startup-v1'
const STATIC_ASSETS = [
'/',
'/manifest.json',
'/icons/icon-192.png',
'/icons/icon-512.png',
]
declare const self: ServiceWorkerGlobalScope
// Install: cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS)
})
)
})
// Fetch: serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
// Skip non-GET requests
if (event.request.method !== 'GET') return
event.respondWith(
caches.match(event.request).then((cached) => {
// Return cached or fetch
const networked = fetch(event.request)
.then((response) => {
// Cache new requests
const cacheCopy = response.clone()
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, cacheCopy)
})
return response
})
.catch(() => {
// Offline fallback
return caches.match('/offline')
})
return cached || networked
})
)
})
// Activate: clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys
.filter((key) => key !== CACHE_NAME)
.map((key) => caches.delete(key))
)
})
)
})
Register Service Worker
// components/RegisterSW.tsx
'use client'
import { useEffect } from 'react'
export function ServiceWorkerRegistration() {
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
console.log('SW registered:', registration)
})
.catch((error) => {
console.log('SW registration failed:', error)
})
}
}, [])
return null
}
PWA Features
Offline Support
// Detect offline status
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true)
useEffect(() => {
setIsOnline(navigator.onLine)
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
}
Push Notifications
// Request notification permission
async function requestNotificationPermission() {
if (!('Notification' in window)) {
console.log('Notifications not supported')
return
}
const permission = await Notification.requestPermission()
if (permission === 'granted') {
console.log('Notification permission granted')
// Subscribe to push service
await subscribeToPush()
}
}
// Subscribe to push notifications
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!
),
})
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
})
}
Next.js PWA Plugin
// next.config.js with PWA
const withPWA = require('@ducanh2912/next-pwa').default({
dest: 'public',
disable: process.env.NODE_ENV === 'development',
})
/** @type {import('next').NextConfig} */
const nextConfig = {
// Your config
}
module.exports = withPWA(nextConfig)
Cost Analysis
# PWA vs Native App cost comparison
pwa:
development_time: "1-2 months"
development_cost: "$5,000-15,000"
app_store: "No"
updates: "Instant"
maintenance: "Single codebase"
native_ios:
development_time: "2-4 months"
development_cost: "$15,000-30,000"
app_store: "Yes (7+ days review)"
updates: "App Store review"
maintenance: "iOS team"
native_android:
development_time: "2-4 months"
development_cost: "$15,000-30,000"
app_store: "Yes (hours to days)"
updates: "Play Store review"
maintenance: "Android team"
Key Takeaways
- PWAs are free - No app store fees
- Instant updates - No review process
- Works offline - Service workers
- Mobile reach - Add to home screen
Comments