
Eine Progressive Web App (PWA) ist eine Webanwendung, die moderne Browserfunktionen nutzt, um ein App-ähnliches Erlebnis zu bieten. PWAs sind zuverlässig, schnell und ansprechend – sie kombinieren die Reichweite des Webs mit den Funktionen nativer mobiler Anwendungen.
Eine Progressive Web App (PWA) ist eine Webanwendung, die moderne Browserfunktionen nutzt, um ein App-ähnliches Erlebnis zu bieten. PWAs sind zuverlässig, schnell und ansprechend – sie kombinieren die Reichweite des Webs mit den Funktionen nativer mobiler Anwendungen.
PWAs lösen ein grundlegendes Problem: Benutzer möchten die sofortige Verfügbarkeit des Webs (keine Installation, verlinkbar, immer aktuell), erwarten aber die Leistung und Funktionen nativer Apps (Offline-Unterstützung, Push-Benachrichtigungen, Präsenz auf dem Startbildschirm). PWAs bieten beides.
Der Begriff „Progressive Web App“ wurde 2015 von Alex Russell und Frances Berriman von Google geprägt. Eine PWA muss diese 10 Merkmale erfüllen:
| # | Prinzip | Warum es wichtig ist |
|---|---|---|
| 1 | Progressiv – Funktioniert für jeden | Anmutige Verschlechterung bei älteren Browsern |
| 2 | Responsiv – Passt auf jeden Bildschirm | Desktop, Tablet, Mobil |
| 3 | Konnektivitätsunabhängig – Funktioniert offline | Caching von Servicemitarbeitern |
| 4 | App-ähnlich – Natives App-Feeling | Shell-Architektur, reibungslose Navigation |
| 5 | Frisch – Immer aktuell | Service-Worker-Update-Lebenszyklus |
| 6 | Sicher – Wird über HTTPS bereitgestellt | Verhindert Manipulationen, erforderlich für Servicemitarbeiter |
| 7 | Auffindbar – SEO-freundlich | Suchmaschinen indizieren PWAs |
| 8 | Wieder aktivierbar – Push-Benachrichtigungen | Binden Sie Benutzer wie native Apps erneut ein |
| 9 | Installierbar – Zum Startbildschirm hinzufügen | Web-App-Manifest |
| 10 | Verlinkbar – Über URL teilen | Kein App-Store erforderlich |
Ein Service Worker ist eine JavaScript-Datei, die getrennt von der Webseite im Hintergrund ausgeführt wird. Es fungiert als programmierbarer Netzwerk-Proxy, der Anfragen abfängt und Offline-Funktionalität, Push-Benachrichtigungen und Hintergrundsynchronisierung ermöglicht.
Installing → Installed → Activating → Activated → Idle → Terminated
│ │
└─── (new version detected) ──────────────┘
└─── Waiting (until all tabs close) ─────┘
// service-worker.js
const CACHE_NAME = 'my-pwa-v2';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html',
'/icons/icon-192.png'
];
// Install event — cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log('Caching app shell');
return cache.addAll(ASSETS_TO_CACHE);
})
);
// Force new service worker to activate immediately
self.skipWaiting();
});
// Activate event — clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
// Take control of all open pages immediately
clients.claim();
});
// Fetch event — intercept network requests
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
// Return cached response if available
if (cachedResponse) {
return cachedResponse;
}
// Otherwise fetch from network
return fetch(event.request).then(networkResponse => {
// Don't cache non-GET or error responses
if (!event.request.url.includes('/api/')) {
return networkResponse;
}
// Cache API responses for offline use
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
return networkResponse;
}).catch(() => {
// Network failed — return offline fallback
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
return new Response('Offline', { status: 503 });
});
})
);
});
| Strategie | Beschreibung | Anwendungsfall |
|---|---|---|
| Zuerst zwischenspeichern | Überprüfen Sie den Cache und greifen Sie auf das Netzwerk zurück | Statische Assets (CSS, JS, Bilder) |
| Netzwerk zuerst | Versuchen Sie es mit dem Netzwerk und greifen Sie auf den Cache zurück | API-Aufrufe, dynamische Inhalte |
| Veraltet während der erneuten Validierung | Cache zurückgeben, im Hintergrund aktualisieren | Newsfeeds, Blogbeiträge |
| Nur Netzwerk | Immer aus dem Netzwerk abrufen | Sensible Daten (Banking) |
| Nur Cache | Niemals aus dem Netzwerk abrufen | App-Shell |
// Stale-while-revalidate strategy
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
}).catch(() => cachedResponse);
return cachedResponse || fetchPromise;
})
);
});
Das Manifest ist eine JSON-Datei, die steuert, wie die PWA bei der Installation angezeigt wird:
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "An amazing PWA that works offline",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3367D6",
"orientation": "portrait-primary",
"scope": "/",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["productivity", "utilities"],
"lang": "en-US",
"dir": "ltr",
"prefer_related_applications": false
}
Manifeste Eigenschaften erklärt:
scope: „/“ – Welche URLs sind Teil der PWA.Servicemitarbeiter benötigen einen sicheren Kontext. HTTPS ist obligatorisch für:
# Redirect HTTP to HTTPS
# Nginx
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
Die App-Shell besteht aus dem minimalen HTML-, CSS- und JavaScript-Code, der zum Rendern der Benutzeroberfläche erforderlich ist. Es wird beim ersten Laden zwischengespeichert und dient als Grundlage der PWA.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367D6">
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/icons/icon-192.png" sizes="192x192">
<link rel="apple-touch-icon" href="/icons/icon-192.png">
<title>My PWA</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header id="app-header">My PWA</header>
<main id="app-content"></main>
<footer id="app-footer">© 2026</footer>
<!-- Register service worker -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.error('SW registration failed:', err));
});
}
</script>
<script src="/app.js"></script>
</body>
</html>
// app.js — Fetch content with offline support
const CACHE_KEY = 'content-cache';
async function loadContent(url) {
try {
const response = await fetch(url);
const data = await response.json();
// Cache for offline
const cache = await caches.open(CACHE_KEY);
cache.put(url, new Response(JSON.stringify(data)));
renderContent(data);
} catch (error) {
// Offline — load from cache
const cached = await caches.match(url);
if (cached) {
const data = await cached.json();
renderContent(data);
showOfflineIndicator();
} else {
showOfflineMessage();
}
}
}
// Request permission
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
// Get push subscription
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription)
});
}
}
// service-worker.js — Handle push events
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
vibrate: [100, 50, 100],
data: { url: data.url },
actions: [
{ action: 'open', title: 'Open' },
{ action: 'close', title: 'Dismiss' }
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'open' || !event.action) {
clients.openWindow(event.notification.data.url);
}
});
| Funktion | PWA | Native App |
|---|---|---|
| Verteilung | URL (kein Shop erforderlich) | App Store, Google Play |
| Reibung einbauen | Ein Fingertipp (Browser-Eingabeaufforderung) | Herunterladen, installieren, Berechtigungen |
| Dateigröße | KB zu MB | 10 MB bis 500 MB+ |
| Aktualisierungen | Instant (Servicemitarbeiter) | Via App-Store-Rezension |
| Offline | Ja (Servicemitarbeiter) | Ja (eingebaut) |
| Push-Benachrichtigungen | Ja | Ja |
| Geräte-APIs | Kamera, GPS, Beschleunigungsmesser, Zahlung | Voller Zugriff auf alle Geräte-APIs |
| Bluetooth | Web-Bluetooth (begrenzt) | Vollständige Bluetooth-API |
| Hintergrundaufgaben | Hintergrundsynchronisierung (begrenzt) | Vollständige Hintergrundausführung |
| Leistung | Nahezu nativ (JavaScript/WebAssembly) | Native (kompilierter Code) |
| SEO | Indexierbar durch Suchmaschinen | Nicht indexierbar |
| Entwicklungskosten | Einzelne Codebasis | iOS + Android (oder plattformübergreifend) |
| Benutzerbindung | Niedriger (leichter zu verlassen) | Höher (installiert = festgeschrieben) |
| Metrisch | Vor PWA | Nach PWA | Verbesserung |
|---|---|---|---|
| Erster Ladevorgang | 5s | 1,2s | 76 % schneller |
| Nachfolgende Ladungen | 3s | 0,3s | 90 % schneller |
| Absprungrate | 45% | 20% | 55 % Ermäßigung |
| Conversion-Rate | 2% | 3.5% | Steigerung um 75 % |
| Seiten pro Sitzung | 3 | 5 | Steigerung um 66 % |
Ergebnisse aus der Praxis:
| Unternehmen | Schlüsselmetrik | Verbesserung |
|---|---|---|
| Twitter (X) Lite | Pro Sitzung gesendete Tweets | Steigerung um 75 % |
| Zeitaufwand | Steigerung um 40 % | |
| Uber | Buchungsabschluss | Reibungslos, auch bei 2G |
| Starbucks | Täglich aktive Benutzer | 2x Erhöhung |
| Alibaba | Conversion-Rate | Steigerung um 76 % |
# Run Lighthouse from CLI
npx lighthouse https://example.com --view --preset=desktop
# PWA-specific checks:
# - ✅ Registers a service worker
# - ✅ Responds with 200 when offline
# - ✅ Web app manifest exists
# - ✅ Manifest has display: standalone
# - ✅ Served over HTTPS
# - ✅ Redirects HTTP to HTTPS
# - ✅ Configurable start URL
# - ✅ Icons provided (192px and 512px)
## PWA Pre-Flight Checklist
- [ ] HTTPS enabled
- [ ] Service worker registered and active
- [ ] Pages load offline (test with airplane mode)
- [ ] Manifest configured with correct icons
- [ ] "Add to Home Screen" prompt appears
- [ ] PWA opens in standalone mode with no address bar
- [ ] Splash screen displays correctly
- [ ] Push notifications work
- [ ] All pages are responsive (mobile, tablet, desktop)
- [ ] Performance budget met (Lighthouse score > 90)
- [ ] Cross-browser tested (Chrome, Firefox, Safari, Edge)
- [ ] Accessibility audit passes
// Register a sync event
async function scheduleSync() {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-pending-data');
}
// Service worker handles sync
self.addEventListener('sync', event => {
if (event.tag === 'sync-pending-data') {
event.waitUntil(syncPendingData());
}
});
// Request periodic sync (requires user engagement)
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.register('update-content', {
minInterval: 24 * 60 * 60 * 1000 // Once per day
});
// Share content using native share sheet
async function shareContent(title, text, url) {
if (navigator.share) {
await navigator.share({ title, text, url });
} else {
// Fallback to custom share UI
copyToClipboard(url);
}
}
// Set app icon badge
if (navigator.setAppBadge) {
await navigator.setAppBadge(5); // Show "5" on icon
} else if (navigator.clearAppBadge) {
await navigator.clearAppBadge();
}
| Falle | Problem | Lösung |
|---|---|---|
| Übercaching | Die App stellt veraltete Inhalte bereit | Versions-Caches, Cache-First für die Shell und Network-First für den Inhalt implementieren |
| Kein Offline-Fallback | Weißer Bildschirm offline | Cachen Sie /offline.html immer mit einer aussagekräftigen Nachricht |
| Cache-Busting-Fehler | Alter Servicemitarbeiter bleibt bestehen | Verwenden Sie SkipWaiting() und Clients.claim() |
| Probleme mit dem Begrüßungsbildschirm | Falsche Designfarbe oder falsches Symbol | Konfigurieren Sie das Manifest richtig und verwenden Sie maskierbare Symbole |
| Safari-Kompatibilität | Einige Funktionen eingeschränkt (Push, regelmäßige Synchronisierung) | Progressive Verbesserung – sanfter Abbau |
| Große Cache-Größe | Speicherkontingent überschritten (50 MB pro Ursprung) | Alte Caches bereinigen, Cache-Größe begrenzen |
| Start-URL fehlt | Der Startbildschirm öffnet eine falsche Seite | Geben Sie im Manifest immer start_url an |
Progressive Web Apps repräsentieren das Beste aus beiden Welten – die Reichweite und Zugänglichkeit des Webs kombiniert mit den Funktionen und dem Benutzererlebnis nativer Anwendungen.
PWAs sind nicht in jedem Szenario ein Ersatz für native Apps – aber für die meisten inhaltsgesteuerten und nützlichen Anwendungen bieten sie ein hervorragendes Benutzererlebnis zu einem Bruchteil der Entwicklungskosten.
Noch keine freigegebenen Kommentare sichtbar. Neue Antworten können moderiert werden.