What Makes a Progressive Web App#

A Progressive Web App is a web application that uses modern browser APIs and a strategic design approach to deliver a native-app-like experience. The term was coined by Alex Russell and Frances Berriman in 2015, but the underlying principles are rooted in the web’s core strengths: linkability, discoverability, and universal access.

A PWA is not a framework or a specific technology. It is a set of characteristics:

CharacteristicMeaning
ProgressiveWorks for every user regardless of browser, using progressive enhancement
ResponsiveAdapts to any form factor — phone, tablet, desktop
Connectivity-independentFunctions offline or on unreliable networks via service workers
App-likeFeels like a native app with app-shell architecture and smooth interactions
FreshAlways up to date thanks to service worker update mechanisms
SafeServed over HTTPS to prevent snooping and content tampering
DiscoverableIdentifiable as an “application” by search engines via the web app manifest
Re-engageableSupports push notifications to bring users back
InstallableUsers can add it to their home screen without an app store
LinkableShareable via URL with zero installation friction

The Three Technical Pillars#

Every PWA rests on three technical requirements:

  1. HTTPS — the entire origin must be served over a secure connection (localhost is exempted for development).
  2. A web app manifest — a JSON file that describes the application to the browser.
  3. A service worker — a JavaScript file that acts as a programmable network proxy.

The Web App Manifest#

The manifest is a JSON file (conventionally manifest.json or manifest.webmanifest) linked from every HTML page:

<link rel="manifest" href="/manifest.webmanifest">

Minimal Manifest#

{
  "name": "My Progressive App",
  "short_name": "MyApp",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#1a73e8",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Key Manifest Fields#

FieldRequiredPurpose
nameYesFull application name shown during install
short_nameRecommendedDisplayed on the home screen (max ~12 characters)
start_urlYesURL opened when the user launches the installed app
displayYesstandalone, fullscreen, minimal-ui, or browser
background_colorRecommendedSplash screen background while the app loads
theme_colorRecommendedBrowser toolbar colour / OS status bar tint
iconsYesArray of icon objects; at minimum 192×192 and 512×512
scopeOptionalURL scope that constrains navigation within the app
descriptionOptionalUsed by app stores and install prompts
screenshotsOptionalRich install UI on Android (richer install dialog)

Display Modes#

fullscreen   → No browser UI at all. Entire screen.
standalone   → Looks like a native app. No URL bar.
minimal-ui   → Small navigation controls visible.
browser      → Standard browser tab. Not installable.

You can query the current display mode in CSS:

@media (display-mode: standalone) {
  /* Styles applied only when running as installed PWA */
  .install-banner { display: none; }
}

Installability Criteria#

Browsers use a set of heuristics to decide whether a web app qualifies for the install prompt. Chrome’s current criteria:

  1. The page is served over HTTPS.
  2. A valid web app manifest is present with name or short_name, start_url, display set to standalone/fullscreen/minimal-ui, and at least one icon ≥ 192×192.
  3. A service worker is registered with a fetch event handler.
  4. The user has engaged with the page (interacted at least once).

Controlling the Install Prompt#

Browsers fire a beforeinstallprompt event that you can intercept:

let deferredPrompt;

window.addEventListener('beforeinstallprompt', (e) => {
  // Prevent the default mini-infobar
  e.preventDefault();
  deferredPrompt = e;

  // Show your custom install button
  document.getElementById('install-btn').style.display = 'block';
});

document.getElementById('install-btn').addEventListener('click', async () => {
  if (!deferredPrompt) return;

  deferredPrompt.prompt();
  const { outcome } = await deferredPrompt.userChoice;
  console.log(`User ${outcome === 'accepted' ? 'accepted' : 'dismissed'} install`);
  deferredPrompt = null;
});

Detecting whether the app is already installed:

window.addEventListener('appinstalled', () => {
  console.log('PWA installed successfully');
  // Hide the install button, log analytics event
  document.getElementById('install-btn').style.display = 'none';
});

App Shell Architecture#

The app shell model separates the minimal UI skeleton (HTML, CSS, core JavaScript) from the dynamic content. The shell is cached aggressively on first visit so subsequent loads are instant — even offline.

┌──────────────────────────────────────┐
│           App Shell (cached)         │
│  ┌──────────────────────────────┐    │
│  │   Header / Navigation        │    │
│  ├──────────────────────────────┤    │
│  │                              │    │
│  │   Content Area               │    │ ← Loaded dynamically
│  │   (fetched from network      │    │   from API / cache
│  │    or cache at runtime)      │    │
│  │                              │    │
│  ├──────────────────────────────┤    │
│  │   Footer / Tab Bar           │    │
│  └──────────────────────────────┘    │
└──────────────────────────────────────┘

Implementing an App Shell#

In your service worker install event, pre-cache the shell resources:

const SHELL_CACHE = 'app-shell-v1';
const SHELL_FILES = [
  '/',
  '/index.html',
  '/styles/app.css',
  '/scripts/app.js',
  '/icons/icon-192.png',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_FILES))
  );
});

Then serve the shell from cache in the fetch handler:

self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      caches.match('/index.html').then((response) => response || fetch(event.request))
    );
    return;
  }

  event.respondWith(
    caches.match(event.request).then((response) => response || fetch(event.request))
  );
});

Lighthouse PWA Audit#

Google’s Lighthouse tool audits PWA compliance. Run it from Chrome DevTools (Lighthouse panel) or the command line:

npx lighthouse https://example.com --only-categories=pwa --output=json

Key Audit Checks#

AuditWhat It Verifies
InstallableValid manifest, service worker with fetch handler, HTTPS
Splash screenbackground_color, theme_color, name, 512×512 icon
Themed address bartheme_color in manifest and <meta name="theme-color">
Content sized correctlyViewport meta tag present, content fits without horizontal scroll
Redirects HTTP → HTTPSAll HTTP requests redirect to HTTPS
Offline fallbackService worker returns a 200 when offline
Maskable iconAt least one icon with "purpose": "maskable" for adaptive display

Maskable Icons#

Android adaptive icons require a “safe zone” — the inner 80% of the image. Add a maskable icon to your manifest:

{
  "src": "/icons/icon-maskable-512.png",
  "sizes": "512x512",
  "type": "image/png",
  "purpose": "maskable"
}

Use maskable.app to preview how your icon looks in different shapes.


PWA vs Native vs Hybrid#

DimensionPWANativeHybrid (Capacitor/Cordova)
DistributionURL, no app store requiredApp Store / Play StoreApp Store / Play Store
InstallationBrowser prompt, instantDownload + installDownload + install
Update mechanismService worker, instantStore review + user updateStore review + user update
Offline supportService worker + Cache APIFull device API accessPlugin-dependent
Hardware accessLimited (camera, geolocation, sensors)Full (Bluetooth, NFC, file system)Plugin-dependent
PerformanceNear-native with good patternsBest possibleWorse than native
Development costSingle codebase (web)Per-platform codebaseSingle codebase + plugins
DiscoverabilitySearch engines + URL sharingApp store searchApp store search
Push notificationsSupported (not on iOS Safari before 16.4)Full supportFull support
Storage quotaBrowser-managed, can be evictedUnlimitedPlugin-dependent

When to Choose a PWA#

  • Your audience is primarily on the web and you want zero-friction access.
  • You need fast iteration cycles without app store review delays.
  • Your functionality can be achieved with available web APIs.
  • You want a single codebase serving desktop and mobile.

When Native Is Still Necessary#

  • You need deep hardware integration (Bluetooth LE peripherals, ARKit/ARCore, NFC writes).
  • Your app is performance-critical (3D games, real-time video processing).
  • You require persistent background execution beyond what the web platform allows.
  • Your business model depends on app store visibility and in-app purchases.

Key Takeaways#

  • A PWA is defined by characteristics (reliable, fast, engaging), not by a specific framework or library — any web app can become progressive incrementally.
  • The three technical pillars are HTTPS, a web app manifest, and a service worker with a fetch event handler.
  • The web app manifest controls how the installed app appears: its name, icon, splash screen, display mode, and theme colour.
  • App shell architecture separates the cacheable UI skeleton from dynamic content, enabling instant loads on repeat visits and offline access.
  • Lighthouse provides automated auditing of PWA compliance — run it early and often during development.
  • PWAs trade some native capabilities for zero-friction distribution, instant updates, and a single codebase — the right choice depends on your audience and required device APIs.