App-Entwicklung: Responsive Web Apps, PWA und Service Worker
Progressive Web Apps (PWAs) kombinieren das Beste von Web- und nativen Apps: Sie laufen im Browser, sind installierbar, arbeiten offline und senden Push-Benachrichtigungen. Sie bieten die Reichweite des Webs mit der UX einer nativen App — ohne App-Store-Abnahmeprozess, ohne 30% Provision, ohne Plattform-spezifische Codebases.
In diesem Tutorial lernst Du, wie Du eine PWA baust — vom Manifest über Service Worker bis zu Offline-Daten und Push-Notifications.
TL;DR — PWAs in 90 Sekunden
Eine PWA ist eine Web-App, die installierbar ist, offline funktioniert und sich wie eine native App verhält.
---
Die 3 Säulen: Web App Manifest (Installierbarkeit), Service Worker (Offline/Caching), Responsive Design (alle Geräte).
Service Worker: JavaScript im Hintergrund, der Netzwerk-Requests abfängt und aus dem Cache beantwortet.
Caching-Strategien: Cache-First (statisch), Network-First (dynamisch), Stale-While-Revalidate (beides).
Ende der kompakten Erklärung!
Was ist eine Progressive Web App?
Eine PWA ist eine Web-Anwendung, die:
- Installierbar ist — auf Desktop und Mobile, erscheint im App-Launcher / Home-Screen
- Offline funktioniert — Service Worker cachen Ressourcen und Daten
- Push-Benachrichtigungen empfangen kann — auch wenn die App geschlossen ist
- Responsive ist — passt sich an jedes Gerät an (Mobile, Tablet, Desktop)
- Sicher ist — HTTPS ist Pflicht für Service Worker
- App-ähnliche UX bietet — Fullscreen, Splash-Screen, keine Browser-UI
PWA vs. Native App vs. Web-App
| Feature | PWA | Native App | Web-App |
|---|---|---|---|
| Installation | Browser-Prompt | App Store | Nicht installierbar |
| Offline | ✅ (Service Worker) | ✅ | ❌ |
| Push-Notifications | ✅ | ✅ | ❌ |
| Hardware-Zugriff | Eingeschränkt | Voll | Sehr eingeschränkt |
| App-Store | Optional | Pflicht | Nicht nötig |
| Entwicklung | Eine Codebase | Pro Plattform | Eine Codebase |
| Update | Automatisch (Browser) | App Store Review | Automatisch |
| Kosten | Kostenlos | 99$/Jahr (Apple), 25$ (Google) | Kostenlos |
Die drei Säulen einer PWA
1. Web App Manifest
Das Manifest ist eine JSON-Datei, die definiert, wie die App installiert wird und aussieht. Ohne Manifest keine Installation.
{
"name": "Meine PWA",
"short_name": "PWA",
"description": "Eine Progressive Web App mit Offline-Funktionalität",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"shortcuts": [
{
"name": "Neue Notiz",
"url": "/new",
"icons": [{ "src": "/icons/shortcut.png", "sizes": "96x96" }]
}
]
}
Wichtige Felder erklärt:
display: "standalone"— App läuft ohne Browser-UI (Adressleiste, Navigation)theme_color— Farbe der Status-Bar auf Mobilepurpose: "maskable"— Icon kann auf verschiedene Formen zugeschnitten werden (Android)shortcuts— Schnellaktionen im App-Launcher (Rechtsklick auf Desktop, Long-Press auf Mobile)
Einbindung im HTML:
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#000000" />
<!-- iOS-spezifisch: -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
Warum die iOS-spezifischen Meta-Tags? Safari ignoriert teilweise das Manifest. Die apple-* Tags stellen sicher, dass die App auf iOS korrekt installiert und dargestellt wird.
2. Service Worker — Das Herzstück
Der Service Worker ist ein JavaScript-Worker, der im Hintergrund läuft — unabhängig von der Web-Seite. Er fungiert als Proxy zwischen der App und dem Netzwerk: Jeder Request läuft durch den Service Worker, der entscheidet, ob er aus dem Cache oder vom Netzwerk beantwortet wird.
Service Worker Lifecycle:
Install → Activate → Fetch/Message Events (laufend)
↓ ↓
Caching Alte Caches löschen
// sw.js — Der Service Worker
const CACHE_NAME = 'my-pwa-v1';
const ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/icons/icon-192.png',
'/offline.html' // Fallback-Seite
];
// INSTALLATION: Assets vorab cachen (Pre-Caching)
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(ASSETS))
.then(() => self.skipWaiting()) // Sofort aktivieren, nicht warten
);
});
// AKTIVIERUNG: Alte Caches löschen (Versionierung)
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys
.filter(key => key !== CACHE_NAME) // Alle außer aktueller Cache
.map(key => caches.delete(key)) // Löschen
);
}).then(() => self.clients.claim()) // Sofort Kontrolle übernehmen
);
});
// FETCH: Cache-First-Strategie (für statische Assets)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response; // Aus Cache
}
// Nicht im Cache → vom Netzwerk
return fetch(event.request).catch(() => {
// Netzwerk fehlt → Offline-Fallback
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});
Was hier passiert:
- Install: Beim ersten Besuch werden alle statischen Assets (HTML, CSS, JS, Icons) in den Cache gespeichert.
skipWaiting()sorgt dafür, dass der Service Worker sofort aktiv wird. - Activate: Wenn eine neue Version des Service Workers deployed wird (
CACHE_NAME = 'my-pwa-v2'), löscht der neue Service Worker alle alten Caches. - Fetch: Jeder Request wird zuerst im Cache gesucht. Gefunden → aus Cache antworten (schnell, offline). Nicht gefunden → vom Netzwerk laden. Netzwerk fehlt → Offline-Seite zeigen.
3. Responsive Design
Responsive Design ist nicht PWA-spezifisch, aber essential — eine PWA muss auf Mobile, Tablet und Desktop gut aussehen.
/* Mobile-First: Standard-Styles für Mobile */
.container {
width: 100%;
padding: 1rem;
font-size: 16px;
}
/* Navigation: Hamburger auf Mobile, horizontal auf Desktop */
.nav {
display: flex;
flex-direction: column; /* Mobile: untereinander */
}
/* Tablet (768px+) */
@media (min-width: 768px) {
.container {
max-width: 720px;
margin: 0 auto;
}
.nav {
flex-direction: row; /* Tablet: nebeneinander */
}
}
/* Desktop (1024px+) */
@media (min-width: 1024px) {
.container {
max-width: 960px;
}
}
/* Großer Desktop (1440px+) */
@media (min-width: 1440px) {
.container {
max-width: 1200px;
}
}
Mobile-First-Prinzip: Starte mit Mobile-Styles (kleinste Bildschirme) und füge dann mit min-width Media Queries größere Bildschirme hinzu. Das ist effizienter als Desktop-First mit max-width, weil Mobile-Geräte weniger CSS verarbeiten müssen.
Service Worker registrieren
Der Service Worker muss aus der Haupt-App heraus registriert werden:
// app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registriert:', registration.scope);
// Update-Erkennung
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// Neue Version verfügbar — User benachrichtigen
showUpdateNotification();
}
});
});
})
.catch(error => {
console.error('SW Registrierung fehlgeschlagen:', error);
});
});
}
// Update anwenden
function applyUpdate() {
navigator.serviceWorker.getRegistration().then(reg => {
if (reg.waiting) {
reg.waiting.postMessage({ type: 'SKIP_WAITING' });
}
});
window.location.reload();
}
Wichtig:
- Service Worker müssen über HTTPS ausgeliefert werden (Ausnahme:
localhostfür Entwicklung) - Der Pfad zum Service Worker bestimmt seinen Scope —
/sw.jskontrolliert die gesamte Domain - Service Worker werden beim ersten Besuch installiert und bei jedem folgenden Besuch aus dem Cache geladen
Caching-Strategien — Welche für welchen Fall?
Die Wahl der richtigen Caching-Strategie ist entscheidend für die Performance und Offline-Fähigkeit Deiner PWA.
Cache-First (für statische Assets)
Antwortet immer aus dem Cache, wenn verfügbar. Nur wenn nicht im Cache, geht es ans Netzwerk.
// Ideal für: CSS, JS, Fonts, Icons — Dateien die sich selten ändern
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
);
});
Wann verwenden? Statische Assets, die sich zwischen Deployments nicht ändern. Bei einem neuen Deployment wird der Cache-Name geändert und alte Caches werden gelöscht.
Network-First (für dynamische Inhalte)
Versucht zuerst das Netzwerk. Schlägt fehl (offline), fällt auf den Cache zurück.
// Ideal für: API-Requests, News-Feeds, User-Daten
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then(response => {
// Erfolgreiche Antwort in Cache speichern
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
})
.catch(() => {
// Offline → aus Cache
return caches.match(event.request);
})
);
});
Wann verwenden? Dynamische Inhalte, die immer aktuell sein sollen, aber offline noch lesbar sein können.
Stale-While-Revalidate (Beste von beiden)
Antwortet sofort aus dem Cache (schnell) und holt im Hintergrund eine neue Version (aktuell).
// Ideal für: Bilder, Fonts, nicht-kritische Assets
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(response => {
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return response;
}).catch(() => cached);
return cached || fetchPromise;
})
);
});
Wann verwenden? Assets, die sich manchmal ändern, aber bei denen eine leicht veraltete Version akzeptabel ist. Der User bekommt sofort eine Antwort und beim nächsten Besuch die aktuelle Version.
Strategie-Vergleich
| Strategie | Geschwindigkeit | Aktualität | Offline | Eignung |
|---|---|---|---|---|
| Cache-First | Sehr schnell | Veraltet bis Update | ✅ | Statische Assets |
| Network-First | Langsam (Netzwerk) | Immer aktuell | ✅ (alter Cache) | Dynamische Inhalte |
| Stale-While-Revalidate | Sehr schnell | Etwas veraltet | ✅ | Bilder, Fonts |
Multi-Strategie-Ansatz
In der Praxis nutzt Du verschiedene Strategien für verschiedene Request-Typen:
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// API-Requests: Network-First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
return;
}
// Bilder: Stale-While-Revalidate
if (event.request.destination === 'image') {
event.respondWith(staleWhileRevalidate(event.request));
return;
}
// Alles andere: Cache-First
event.respondWith(cacheFirst(event.request));
});
Push-Benachrichtigungen
Push-Notifications ermöglichen es Deiner PWA, User auch dann zu erreichen, wenn die App geschlossen ist. Das erfordert einen Push-Service (z.B. Firebase Cloud Messaging, Web Push).
// 1. Berechtigung anfragen
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.log('User hat Notifications abgelehnt');
return;
}
// 2. Push-Subscription erstellen
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // Notifications müssen sichtbar sein
applicationServerKey: VAPID_PUBLIC_KEY // Öffentlicher VAPID-Key
});
// 3. Subscription an Server senden
await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
console.log('Push-Subscription registriert');
}
// 4. Im Service Worker empfangen
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url },
actions: [
{ action: 'open', title: 'Öffnen' },
{ action: 'close', title: 'Schließen' }
]
})
);
});
// 5. Klick auf Notification behandeln
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
Was Du brauchst:
- VAPID-Keys (generierbar mit
npx web-push generate-vapid-keys) - Einen Server, der Push-Notifications sendet (z.B. mit
web-pushnpm package) - HTTPS (Pflicht für Service Worker)
iOS-Einschränkung: Push-Notifications auf iOS funktionieren erst ab iOS 16.4 und nur, wenn die PWA zum Home-Screen hinzugefügt wurde.
Offline-Daten mit IndexedDB
Der Cache ist gut für statische Assets, aber nicht für strukturierte Daten. Für Offline-Daten (z.B. Todos, Notizen, User-Daten) nutzt Du IndexedDB.
// IndexedDB ist asynchron und callback-basiert — idb macht es Promise-basiert
// npm install idb
import { openDB } from 'idb';
const db = await openDB('my-pwa-db', 1, {
upgrade(db) {
// Object Store erstellen (wie eine Tabelle)
const store = db.createObjectStore('todos', { keyPath: 'id' });
// Index für schnellere Suchen
store.createIndex('by-status', 'status');
store.createIndex('by-date', 'createdAt');
}
});
// Daten speichern (offline)
async function saveTodo(todo) {
await db.put('todos', {
id: crypto.randomUUID(),
title: todo.title,
status: 'pending',
createdAt: Date.now()
});
}
// Daten lesen
async function getTodos() {
return await db.getAll('todos');
}
// Nach Status filtern
async function getPendingTodos() {
return await db.getAllFromIndex('todos', 'by-status', 'pending');
}
// Daten löschen
async function deleteTodo(id) {
await db.delete('todos', id);
}
Cache vs. IndexedDB:
- Cache API: Für HTTP-Responses (HTML, CSS, JS, Bilder). Key = Request, Value = Response.
- IndexedDB: Für strukturierte Daten (JSON-Objekte, User-Daten, Offline-Changes). Key = beliebiger Key, Value = beliebiges Objekt.
Background Sync (Offline-Änderungen synchronisieren)
Wenn der User offline Änderungen macht, können diese mit Background Sync automatisch synchronisiert werden, sobald wieder online:
// In der App: Sync registrieren
async function syncTodos() {
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('sync-todos');
}
// Im Service Worker: Sync-Event behandeln
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-todos') {
event.waitUntil(syncTodosWithServer());
}
});
async function syncTodosWithServer() {
const pendingTodos = await db.getAllFromIndex('todos', 'by-status', 'pending');
for (const todo of pendingTodos) {
await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(todo)
});
await db.put('todos', { ...todo, status: 'synced' });
}
}
PWA mit Frameworks
Vite PWA Plugin (empfohlen für Vite-Projekte)
// vite.config.js
import { VitePWA } from 'vite-plugin-pwa';
export default {
plugins: [
VitePWA({
registerType: 'autoUpdate', // Automatische Updates
manifest: {
name: 'Meine PWA',
short_name: 'PWA',
theme_color: '#000000',
icons: [
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 50, maxAgeSeconds: 3600 }
}
}
]
}
})
]
};
Vorteil: Vite PWA generiert den Service Worker automatisch mit Workbox. Du musst keinen manuellen Service Worker schreiben.
Next.js PWA
npx create-next-app@latest my-pwa --typescript
npm install @ducanh2912/next-pwa
// next.config.js
const withPWA = require('@ducanh2912/next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
});
module.exports = withPWA({});
Astro PWA
Astro hat eine offizielle PWA-Integration:
npx astro add astro-pwa
PWA testen
Lighthouse Audit
Lighthouse prüft PWA-Konformität und gibt einen Score:
npx lighthouse https://localhost:3000 --view --preset=pwa
Lighthouse prüft:
- Manifest vorhanden und korrekt
- Service Worker registriert
- HTTPS aktiv
- Responsive auf Mobile
- Offline-Funktionalität
- Installierbarkeit
Service Worker Debugging
Chrome DevTools → Application → Service Workers:
- Status anzeigen (aktiv, wartend, installiert)
- “Update on reload” für Entwicklung
- “Bypass for network” um Service Worker zu deaktivieren
- “Unregister” um Service Worker zu entfernen
Manueller Offline-Test
Chrome DevTools → Application → Service Workers → “Offline” checkbox aktivieren. Dann Seite neu laden und prüfen, ob Offline-Mode funktioniert.
App-Installation erkennen
Du kannst den Installations-Prompt abfangen und einen eigenen “Install”-Button anzeigen:
let deferredPrompt;
// Browser zeigt beforeinstallprompt an (vor dem automatischen Prompt)
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // Automatischen Prompt verhindern
deferredPrompt = e;
showInstallButton(); // Eigenen Button anzeigen
});
// User klickt auf Install-Button
installButton.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt(); // Installations-Dialog zeigen
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User hat App installiert');
hideInstallButton();
} else {
console.log('User hat Installation abgelehnt');
}
deferredPrompt = null; // Prompt kann nur einmal verwendet werden
});
// App wurde installiert
window.addEventListener('appinstalled', () => {
console.log('App erfolgreich installiert');
// Analytics-Event senden
});
Wichtig: beforeinstallprompt wird nicht auf iOS ausgelöst. iOS-Nutzer müssen manuell “Zum Home-Bildschirm” wählen.
Prüfungsrelevante Punkte
- PWA: Progressive Web App = installierbar + offline + responsive + sicher (HTTPS)
- 3 Säulen: Web App Manifest (Installation), Service Worker (Offline/Caching), Responsive Design (alle Geräte)
- Manifest: JSON-Datei mit name, icons, display, theme_color, start_url
- Service Worker Lifecycle: Install (pre-cache) → Activate (alte Caches löschen) → Fetch (Requests abfangen)
- Caching-Strategien: Cache-First (statisch), Network-First (dynamisch), Stale-While-Revalidate (beides)
- Push-Notifications: VAPID-Keys, Push-Subscription,
pushEvent im Service Worker - IndexedDB: Offline-Datenbank für strukturierte Daten (im Gegensatz zu Cache API für HTTP-Responses)
- Background Sync: Automatische Synchronisierung von Offline-Änderungen
- HTTPS: Pflicht für Service Worker (Ausnahme: localhost)
- Testing: Lighthouse PWA Audit, Chrome DevTools Application Tab
FAQ
Sind PWAs ein Ersatz für native Apps? Für viele Anwendungsfälle ja — besonders Content-Apps, E-Commerce, Tools. Einschränkungen gibt es bei Hardware-Zugriff (Bluetooth, NFC, Sensoren), Hintergrund-Prozessen und App-Store-Präsenz. Für Spiele und Hardware-nahe Apps bleiben native Apps die bessere Wahl.
Brauche ich ein Framework für PWAs? Nein, PWAs funktionieren mit Vanilla JS — ein Manifest und ein Service Worker reichen. Frameworks (Vite PWA, Next-PWA) erleichtern aber den Setup und generieren den Service Worker automatisch mit Workbox.
Funktionieren PWAs auf iOS?
Ja, mit Einschränkungen. Push-Notifications sind seit iOS 16.4 verfügbar. Background Sync wird nicht unterstützt. Der Installations-Prompt muss manuell über “Teilen → Zum Home-Bildschirm” erfolgen (kein beforeinstallprompt Event).
Wie aktualisiere ich meine PWA?
Bei jedem Deploy: 1) Neue Assets hochladen, 2) Cache-Name im Service Worker erhöhen (z.B. v1 → v2), 3) Browser erkennt beim nächsten Besuch, dass sich der Service Worker geändert hat und installiert die neue Version. Mit skipWaiting() wird die neue Version sofort aktiv.
Was ist der Unterschied zwischen Cache API und IndexedDB? Cache API speichert HTTP-Responses (HTML, CSS, JS, Bilder) — Key ist der Request, Value die Response. IndexedDB speichert strukturierte Daten (JSON-Objekte) mit beliebigen Keys und Indizes. Nutze Cache API für statische Assets und IndexedDB für User-Daten und Offline-Changes.
Kann ich PWA und SPA (React/Vue/Svelte) kombinieren? Ja, das ist sogar der Normalfall. Die SPA läuft als Web-App, der Service Worker cached die SPA-Assets, und das Manifest macht sie installierbar. Vite PWA Plugin unterstützt React, Vue, Svelte und andere Frameworks out of the box.
Empfohlene Literatur
Web-Entwicklung
Bücher über React, Vue, Frontend und Backend
JavaScript: Das umfassende Handbuch. JavaScript lernen und verstehen von Philip Ackermann
Bei Amazon ansehenAffiliate-Link: Bei einem Kauf erhalten wir möglicherweise eine Provision.
React: Das umfassende Handbuch. Moderne Web-Apps mit React, Hooks und TypeScript von Philip Ackermann
Bei Amazon ansehenAffiliate-Link: Bei einem Kauf erhalten wir möglicherweise eine Provision.
Fullstack-Entwicklung: Das Handbuch für Webentwickler
Bei Amazon ansehenAffiliate-Link: Bei einem Kauf erhalten wir möglicherweise eine Provision.





