Progressive Web Apps (PWA): The Hybrid Web Revolution
Turning your PWA into a store-ready app bridges the gap between web and native distribution.

1. Introduction
Progressive Web Apps (PWAs) combine the reach of the web with the capabilities of native applications.
They load instantly, work offline, send notifications, and can be installed on any device — all using standard web technologies: HTML, CSS, and JavaScript.
In 2025, PWAs are no longer an experiment. They are deployed in production by global brands such as Twitter, Starbucks, Uber, and Pinterest.
They solve the complexity of maintaining multiple native apps while offering near-native performance and usability.
2. Definition and Browser Support
A Progressive Web App (PWA) is a web application that:
Uses a Web App Manifest for metadata.
Registers a Service Worker for caching, offline, and background tasks.
Served over HTTPS.
Is installable and behaves like a native app.
Browser support (2025):
Browser Support Level Notes Chromium (Chrome, Edge, Brave) Full Install prompt, push, background sync Firefox Partial No native installation prompt Safari (iOS / macOS) Partial → improving Offline & push supported since iOS 16.4
3. Real-World Adoption
According to PWA Stats:
Company Impact Key Metric Twitter Lite Lightweight version +65% pages/session Starbucks Works offline 2× faster checkout Uber Web <50 KB core bundle Loads in <3 s on 2G Pinterest Rebuilt as a PWA +60% engagement Trivago Offline-ready hotel search +150% engagement time
Insight: PWAs deliver app-store quality performance without app-store friction.
4. Core Building Blocks
4.1 Web App Manifest
The Web App Manifest is a JSON file that defines your app’s metadata and tells the browser how to install and display it.
It connects your web experience to native-like behavior — title, icons, splash screens, colors, and launch parameters.
Example:
{
"name": "PWA App",
"short_name": "PWA",
"id": "com.app.pwa",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1a1a1a",
"icons": [
{ "src": "/images/favicon/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/images/favicon/icon-512.png", "sizes": "512x512", "type": "image/png" }
],
"screenshots": [
{ "src": "/images/screenshots/mobile.png", "sizes": "1080x1920" },
{ "src": "/images/screenshots/desktop.png", "sizes": "1920x1080" }
],
"shortcuts": [
{ "name": "About", "url": "/about" },
{ "name": "Contact", "url": "/contact" }
]
}
Manifest Field Overview
name — Full app name shown in the install interface. Example:
"App PWA"short_name — Label displayed on the home screen or dock. Example:
"App"id — Unique identifier used for store packaging. Example:
"com.quran.pwa"start_url — URL the app loads when opened. Example:
"/"scope — Navigation boundary defining what the PWA controls. Example:
"/"display — Display mode such as
fullscreen,standalone,minimal-ui, orbrowser. Example:"standalone"orientation — Locks the screen orientation on launch. Example:
"portrait"background_color — Background color shown on the splash screen. Example:
"#ffffff"theme_color — Browser UI theme color. Example:
"#1a1a1a"description — Short text shown in install dialogs. Example:
"Offline-ready Quran reader"lang — Application language code. Example:
"ar"dir — Text direction (
ltrorrtl). Example:"rtl"icons — App icons in multiple sizes (required: 192×192 and 512×512). Example:
"/icons/512x512.png"screenshots — Images shown in install banners (Android). Example:
"/images/screenshot-1.png"shortcuts — Quick access actions from long press or context menu. Example:
"About"categories — Used for app store classification. Example:
"education"
Minimum Installable Manifest
To qualify as installable across modern browsers, your manifest must include:
nameshort_nameicons(must include 192×192 and 512×512 PNGs)start_urldisplaytheme_colorServed over HTTPS
Additional fields like id, shortcuts, and screenshots enhance install banners and store packaging (e.g., with PWA Builder)
4.2 Service Worker
A Service Worker is a background script that sits between your web app and the network.
It intercepts requests, caches assets, and enables offline behavior, background sync, and push notifications — all without user interaction.
Core Concepts
Feature Description Typical Use Lifecycle Independent thread with install → activate → fetch phases Controls app updates Caching Stores static and dynamic assets using the Cache API Offline access, faster reloads Intercepting Fetch Handles every network request Choose when to serve cache or fetch fresh Offline Fallback Returns a predefined HTML when the user is offline fallback.html or custom error UI Background Sync / Push Queues updates or sends notifications when reconnected Messaging, analytics, reminders
Lifecycle Events
Install Event – Pre-caches essential assets (app shell).
Activate Event – Cleans old caches and takes control of open tabs.
Fetch Event – Intercepts requests and applies a caching strategy.
Message / Sync Events – Used for background tasks or two-way communication.
Registration in Your App
Register the Service Worker once from your main script:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/serviceWorker.js')
.then(() => console.log('Service Worker registered'))
.catch((error) => console.error('SW registration failed:', error));
});
}
4.3 Cache Types, Management, and Purging
The Cache Storage API gives PWAs full control over how files, data, and network responses are stored locally.
Proper cache design determines performance, offline reliability, and update frequency.
Types of Cache in PWAs
Cache Type Description Typical Contents Lifetime Static Cache Preloaded during the install event (the “App Shell”) HTML, CSS, JS, icons Until next version release Dynamic Cache Created at runtime as users navigate API responses, images, user content Controlled manually or by quota Runtime Cache Short-lived cache for frequently updated assets Feeds, news, live data Regularly revalidated Third-Party Cache Assets fetched from CDNs or APIs Fonts, analytics scripts Managed by browser
Cache Versioning and Naming
Use versioned cache names to differentiate builds and safely purge old assets:
const STATIC_CACHE = 'quran-cache-v1';
const DYNAMIC_CACHE = 'quran-dynamic-v1';
When deploying a new version:
Increment the cache name.
In the activate event, delete outdated caches.
self.addEventListener('activate', (event) => {
const allowList = [STATIC_CACHE, DYNAMIC_CACHE];
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((key) => !allowList.includes(key)).map((key) => caches.delete(key)))
)
);
});
Cache Purging and Size Limiting
To prevent uncontrolled growth:
Set a maximum cache size.
Remove the oldest entries when exceeding the limit.
const limitCacheSize = async (name, size) => {
const cache = await caches.open(name);
const keys = await cache.keys();
if (keys.length > size) {
await cache.delete(keys[0]);
await limitCacheSize(name, size);
}
};
Combine this with runtime caching (e.g., API responses) to ensure performance without bloating storage.
Best Practices
Precache only essentials: HTML, JS, CSS, icons, manifest.
Separate dynamic data into its own cache.
Purge old caches during activation to avoid conflicts.
Use Workbox for declarative caching and automatic cleanup.
Test offline via Chrome DevTools → Application → Service Workers → Offline.
Effective cache management balances speed, storage efficiency, and update reliability, ensuring users always receive fresh yet instantly loadable experiences.
5. Progressive Web Application — Full Example (step-by-step)
production-ready vanilla JavaScript PWA, providing offline access, caching, installability, and a graceful fallback page.
You can generate icons and screenshots for your manifest via pwabuilder.com/imageGenerator
Project structure (recommended)
/index.html
/fallback.html
/manifest.json
/serviceWorker.js
/js/app.js
/css/style.css
/images/icons/192x192.png
/images/icons/512x512.png
Everything lives at the root so the Service Worker can control all routes.
Step 1 – index.html
The main page registers the Service Worker and links the manifest.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>PWA</title>
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#1a1a1a" />
<link rel="icon" href="/images/icons/192x192.png" />
<link rel="stylesheet" href="/css/style.css" />
</head>
<body>
<h1>PWA</h1>
<p>Offline-ready Application.</p>
<button id="installBtn" hidden>Install App</button>
<script src="/js/app.js" defer></script>
</body>
</html>
Explanation
<link rel="manifest">→ enables installability.<meta name="theme-color">→ sets system UI color.The install button is hidden until
beforeinstallpromptfires.
Step 3 – manifest.json
(Already detailed earlier; skip duplication)
Contains app metadata, icons 192×192 + 512×512, shortcuts, screenshots, etc.
Step 4 – fallback.html
Displayed when offline navigation fails, Keep it lightweight and self-contained so it loads instantly even when offline.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Offline Mode</title>
<style>
body {
background: #eceff1;
font-family: Roboto, Arial, sans-serif;
margin: 0;
}
#msg {
max-width: 360px;
margin: 100px auto;
padding: 32px;
background: #fff;
border-radius: 6px;
text-align: center;
}
a {
display: block;
margin-top: 20px;
background: #039be5;
color: #fff;
text-decoration: none;
padding: 12px;
border-radius: 4px;
}
</style>
</head>
<body>
<div id="msg">
<h1>You’re Offline</h1>
<p>This page can’t be displayed without an internet connection.</p>
<a href="/index.html">Return to Home</a>
</div>
</body>
</html>
Keep it lightweight and self-contained so it loads instantly even when offline.
Step 5 – serviceWorker.js
Handles caching and offline logic.
/* serviceWorker.js */
const STATIC_CACHE = 'quran-static-v1';
const DYNAMIC_CACHE = 'quran-dynamic-v1';
const MAX_DYNAMIC_ITEMS = 30;
const ASSETS = [
'/',
'/index.html',
'/fallback.html',
'/css/style.css',
'/images/icons/192x192.png',
'/images/icons/512x512.png',
'/js/app.js'
];
async function limitCacheSize(name, max) {
const cache = await caches.open(name);
const keys = await cache.keys();
if (keys.length > max) {
await cache.delete(keys[0]);
await limitCacheSize(name, max);
}
}
self.addEventListener('install', (evt) => {
evt.waitUntil(caches.open(STATIC_CACHE).then((c) => c.addAll(ASSETS)));
self.skipWaiting();
});
self.addEventListener('activate', (evt) => {
evt.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) =>
k !== STATIC_CACHE && k !== DYNAMIC_CACHE).map((k) => caches.delete(k))
)
).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (evt) => {
const { request } = evt;
if (request.method !== 'GET') return;
if (request.mode === 'navigate') {
evt.respondWith(
(async () => {
try {
const res = await fetch(request);
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, res.clone());
return res;
} catch {
return caches.match('/fallback.html');
}
})()
);
return;
}
evt.respondWith(
caches.match(request).then((cached) => {
return (
cached ||
fetch(request)
.then((res) => {
return caches.open(DYNAMIC_CACHE).then((cache) => {
if (request.url.startsWith(self.location.origin)) {
cache.put(request, res.clone());
limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_ITEMS);
}
return res;
});
})
.catch(() => cached)
);
})
);
});
Explanation
Install → Precache core assets.
Activate → Clean old versions.
Fetch → Cache-first for static, Network-first for navigation.
limitCacheSize prevents unlimited growth.
Step 6 – js/app.js
Registers the Service Worker and handles installation UI.
// js/app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/serviceWorker.js');
console.log('Service Worker registered', reg);
} catch (err) {
console.error('SW registration failed:', err);
}
});
}
// Custom install prompt
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
window.deferredPrompt = e;
const btn = document.getElementById('installBtn');
btn.hidden = false;
btn.addEventListener('click', async () => {
btn.hidden = true;
const prompt = window.deferredPrompt;
prompt.prompt();
await prompt.userChoice;
window.deferredPrompt = null;
});
});
Explanation
Registers
/serviceWorker.jsonce on load.Listens for
beforeinstallpromptand shows the custom Install App button.
Step 7 – css/style.css
Simple theme styling.
body{font-family:system-ui,Arial,sans-serif;margin:0;padding:2rem;background:#fff;color:#222}
h1{color:#1a1a1a}
button{background:#039be5;color:#fff;border:none;padding:.8rem 1.2rem;border-radius:4px;cursor:pointer}
button:hover{background:#0277bd}
Step 8 – Testing Checklist
Test Tool Expected Lighthouse → PWA Audit Chrome DevTools Score > 90 Offline mode DevTools → Network → Offline Fallback shown Install prompt Chromium Android/Desktop “Install App” visible iOS Add to Home Screen Safari Share Menu App icon added Cache inspection DevTools → Application → Cache Storage Static + dynamic entries
Step 9 – Result
You now have a fully functional Progressive Web App that:
Works offline.
Installs on desktop + mobile.
Caches intelligently.
This ipfs.quran.us.kg build demonstrates how far a vanilla PWA can go without any framework or dependency.
Modern frameworks automate manifest injection, SW registration, and caching strategies.
6. Modern Frameworks in Practice
Modern frameworks make building PWAs easier by automating manifest injection, service worker setup, and offline caching.
Framework PWA Support Package Notes Next.js next-pwa Generates manifest, caches routes Expo (React Native) Built-in PWA export One codebase → iOS, Android, Web Angular @angular/pwa schematic Adds manifest, SW, and icons automatically Vite / React vite-plugin-pwa Runtime caching and auto-update SvelteKit / Nuxt 3 Native manifest + SW support File-based configuration
These frameworks hide most low-level details while maintaining flexibility for custom caching and install behavior.
7. Native Packaging and Distribution
PWAs can be wrapped as native apps for app stores using modern tooling.
Tool Platform Output Description Bubblewrap (Google) Android APK / TWA Packages PWAs as Trusted Web Activities PWA Builder (Microsoft) Windows, Android, iOS MSIX / APK / IPA GUI + CLI packaging Capacitor (Ionic) Android, iOS Native wrapper Adds native APIs + store deployment Google Unified Web (UW) Experimental Universal package Future standard bridging PWA + Play Store
Recommended Flow
Validate your PWA’s manifest, service worker, and installability using Lighthouse or Chrome DevTools.
Generate assets and installers with PWA Builder — including icons, screenshots, and store metadata.
Package for app stores using Bubblewrap, Capacitor, or PWA Builder to create ready-to-publish builds for Google Play, Apple App Store, and Microsoft Store.
Submit your packaged PWA through the appropriate store submission process for distribution across platforms.
8. Pros and Cons
Pros Cons Cross-platform single codebase Limited access to native APIs Offline-ready & installable iOS storage and background limits Auto-update via Service Worker Limited monetization channels Lower cost than native apps Discoverability mainly via web, not app stores
9. Conclusion
PWAs are mature, production-ready, and here to stay.
They empower developers to build fast, installable, secure, and cross-platform apps — using only the web stack.
With tools like PWA Builder, Bubblewrap, and Capacitor, PWAs can reach Google Play, Microsoft Store, and even iOS users.
Modern frameworks like Next.js and Expo make this integration seamless.
Start small:
Add a manifest.json, register a serviceWorker.js, and test offline.
Your next web app can be installable in under an hour.
Attribution: cover photo from athree23



