Chapitre 17
17 - PWA (Progressive Web Apps)
17 - PWA (Progressive Web Apps)
17 - PWA : Cours complet
Niveau : Intermédiaire Durée estimée : 4 h Objectif : concevoir une Progressive Web App installable, offline-first, performante et sécurisée, et savoir la comparer aux approches natives (React Native, Flutter).
Table des matières
- Qu'est-ce qu'une PWA ?
- Web App Manifest
- Service Workers
- Stratégies de cache
- Offline : Cache API, IndexedDB, Background Sync
- Notifications push
- Installabilité (A2HS)
- Performance : Core Web Vitals et Lighthouse
- Comparaison PWA / natif / RN / Flutter
- Sécurité d'une PWA
1. Qu'est-ce qu'une PWA ?
1.1 Définition
Une Progressive Web App (PWA) est une application web qui utilise des API navigateur modernes pour offrir une expérience comparable à une application native :
- Installable (icône sur le home screen, fenêtre dédiée)
- Offline-first (fonctionne sans réseau)
- Rapide (chargement immédiat, interactions fluides)
- Engageante (notifications push, badge)
- Sûre (HTTPS obligatoire, permissions)
Les PWA restent du web : un seul code, pas de store obligatoire, mises à jour instantanées.
1.2 Les 3 piliers
Diagramme en cours de génération...
1.3 Capacités (projet Fugu)
Le projet Fugu expose des capacités natives au web, avec permission utilisateur :
| Capacité | API |
|---|---|
| Vibration | navigator.vibrate() |
| Géolocalisation | navigator.geolocation |
| Contacts | navigator.contacts |
| Fichiers | showOpenFilePicker |
| Presse-papiers | navigator.clipboard |
| Badge | navigator.setAppBadge() |
| Web Share | navigator.share() |
Toutes les capacités sont gated par un flag d'origine et une permission explicite.
2. Web App Manifest
2.1 Rôle
Le manifest.json décrit la PWA pour le navigateur et l'OS :
{
"name": "TaskFlow PWA",
"short_name": "TaskFlow",
"description": "Gestion de tâches hors ligne",
"start_url": "/",
"display": "standalone",
"background_color": "#0f172a",
"theme_color": "#3b82f6",
"orientation": "portrait",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["productivity", "utilities"],
"lang": "fr"
}
2.2 Critères d'installation Chrome
Chrome installe une PWA si toutes ces conditions sont réunies :
- HTTPS
- Manifest valide avec
name,icons(192 px et 512 px),start_url,display - Service Worker avec un gestionnaire
fetch - (iOS Safari) : icônes
apple-touch-icon,meta theme-color
2.3 Bonnes pratiques d'icônes
- Toujours fournir l'icône maskable (zone sûre centrale de 80 %).
- Fournir 192 px et 512 px minimum.
- Ajouter
apple-touch-iconpour iOS.
3. Service Workers
3.1 Principes
Un Service Worker (SW) est un script qui s'exécute dans un thread séparé, intercepte les requêtes réseau et permet le cache, l'offline et le push.
Règles fondamentales :
- HTTPS obligatoire (sauf
localhost) - Ne fonctionne que sur la même origine
- Pas d'accès au DOM
- Vie éphémère : tué à chaque inactivité, il ne doit pas contenir d'état global
- Ne s'exécute pas au chargement initial de la page (pas de blocage)
3.2 Cycle de vie
Diagramme en cours de génération...
| Événement | Rôle |
|---|---|
install | Pré-cache des ressources critiques (une seule fois) |
activate | Nettoyage des caches obsolètes, contrôle de la page |
fetch | Stratégie de cache pour chaque requête |
message | Communication avec la page |
push | Réception d'une notification |
sync | Background Sync (tentative de synchronisation) |
3.3 Inscription
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then((reg) => {
console.log('SW enregistré', reg.scope);
});
});
}
3.4 Mise à jour
Chaque navigation déclenche une vérification (updateViaCache). Si le sw.js a changé, un nouvel acteur est installé mais n'active pas immédiatement :
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
).then(() => self.clients.claim())
);
});
Le contrôle de la page n'est pris qu'après le rechargement suivant (ou via
clients.claim()).
3.5 Débogage
- DevTools → Application → Service Workers : inspecter, update, unregister.
- Onglet Cache Storage : inspecter les entrées.
- Onglet Network : distinguer les réponses servies par le SW.
4. Stratégies de cache
4.1 Les 4 stratégies de base
Diagramme en cours de génération...
| Stratégie | Usage | Disponibilité | Fraîcheur |
|---|---|---|---|
| Cache-first | Assets statiques (images, CSS) | Immédiate | Stale |
| Network-first | Pages HTML, API critiques | Si réseau OK | Fraîche |
| Stale-while-revalidate | Données faiblement volatiles | Immédiate | Mise à jour différée |
| Network-only | POST, paiement | Réseau requis | Toujours fraîche |
4.2 Exemple complet
const CACHE = 'taskflow-v1';
const PRECACHE = ['/', '/index.html', '/styles.css', '/app.js'];
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(PRECACHE)));
});
self.addEventListener('fetch', (e) => {
const { request } = e;
if (request.method !== 'GET') return;
if (request.mode === 'navigate') {
// Network-first pour les pages
e.respondWith(
fetch(request)
.then((res) => {
const copy = res.clone();
caches.open(CACHE).then((c) => c.put(request, copy));
return res;
})
.catch(() => caches.match('/index.html'))
);
return;
}
if (new URL(request.url).origin === location.origin) {
// Cache-first pour les assets
e.respondWith(
caches.match(request).then((hit) =>
hit || fetch(request).then((res) => {
const copy = res.clone();
caches.open(CACHE).then((c) => c.put(request, copy));
return res;
})
)
);
}
});
4.3 Cache versionné
Le nom du cache doit contenir la version : taskflow-v1. L'événement activate supprime les anciennes versions → contrôle de la taille (caches.match + quota navigator.storage.estimate()).
5. Offline : Cache API, IndexedDB, Background Sync
5.1 Cache API vs IndexedDB
| API | Type | Usage |
|---|---|---|
| Cache API | Requêtes/réponses HTTP | Assets, pages, réponses d'API |
| IndexedDB | Base NoSQL côté client | Données métier, files d'attente, blobs |
5.2 IndexedDB
// Ouverture
const req = indexedDB.open('taskflow', 1);
req.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('tasks')) {
const store = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true });
store.createIndex('dueDate', 'dueDate');
}
};
// Écriture
function addTask(db, task) {
return new Promise((resolve, reject) => {
const tx = db.transaction('tasks', 'readwrite');
tx.objectStore('tasks').put(task);
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
}
5.3 Background Sync
Permet de mettre en file d'attente une action effectuée hors ligne et de la rejouer quand le réseau revient :
// Page
navigator.serviceWorker.ready.then((reg) => {
return reg.sync.register('sync-tasks'); // 'sync-tasks' < 5 min, sinon 'periodicsync'
});
// Service Worker
self.addEventListener('sync', (e) => {
if (e.tag === 'sync-tasks') {
e.waitUntil(flushQueue());
}
});
Limite : le réseau ne peut pas être détecté directement. Si l'action échoue, l'événement est retenté avec backoff, puis abandonné.
5.4 Periodic Background Sync
Synchronisation périodique (pré-requis periodic-background-sync + permission) :
const status = await navigator.permissions.query({ name: 'periodic-background-sync' });
if (status.state === 'granted') {
await reg.periodicSync.register('refresh-weather', { minInterval: 60 * 60 * 1000 });
}
6. Notifications push
6.1 Le flux push
Diagramme en cours de génération...
6.2 Abonnement (page)
const VAPID_PUBLIC = 'BNx...';
const swReg = await navigator.serviceWorker.ready;
const sub = await swReg.pushManager.subscribe({
userVisibleOnly: true, // exigé par tous les navigateurs
applicationServerKey: VAPID_PUBLIC
});
// Envoi du endpoint au serveur
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(sub.toJSON())
});
6.3 Affichage (Service Worker)
self.addEventListener('push', (e) => {
const data = e.data ? e.data.json() : { title: 'TaskFlow', body: 'Nouvelle tâche' };
e.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge.png',
tag: data.tag || 'default', // remplace la notif ayant le même tag
data: { url: data.url || '/' },
actions: [
{ action: 'open', title: 'Ouvrir' },
{ action: 'dismiss', title: 'Fermer' }
]
})
);
});
self.addEventListener('notificationclick', (e) => {
e.notification.close();
if (e.action === 'dismiss') return;
e.waitUntil(clients.openWindow(e.notification.data.url));
});
6.4 Bonnes pratiques
userVisibleOnly: trueest obligatoire.- Toujours ouvrir une page pertinente au clic (sinon risque de spam).
- Permissions explicites au moment où l'utilisateur est engagé.
- Web Push Protocol : payload chiffré avec VAPID (applicationServerKey).
7. Installabilité (A2HS)
7.1 Add to Home Screen
L'utilisateur peut installer la PWA : icône sur le home screen, ouverture en standalone (sans barre d'URL).
7.2 L'événement beforeinstallprompt
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // ne pas afficher la mini-infobar auto
deferredPrompt = e; // on garde la possibilité d'installer
showInstallButton(); // bouton personnalisé
});
installBtn.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt(); // ouvre le prompt natif
const { outcome } = await deferredPrompt.userChoice;
console.log(outcome); // 'accepted' | 'dismissed'
deferredPrompt = null;
});
window.addEventListener('appinstalled', () => {
console.log('PWA installée');
// analytics
});
7.3 Détection du mode standalone
const isStandalone =
window.matchMedia('(display-mode: standalone)').matches ||
navigator.standalone === true; // iOS Safari
if (isStandalone) { /* UX plein écran */ }
7.4 iOS (Safari)
- Pas d'API
beforeinstallprompt; l'utilisateur utilise Partager → Ajouter à l'écran d'accueil. - Nécessite
apple-touch-iconet<meta name="apple-mobile-web-app-capable" content="yes">. - L'offline via SW est supportée depuis iOS 11.3+.
8. Performance : Core Web Vitals et Lighthouse
8.1 Les Core Web Vitals
| Métrique | Seuil "bon" | Description |
|---|---|---|
| LCP | ≤ 2,5 s | Largest Contentful Paint : contenu principal |
| INP | ≤ 200 ms | Interaction to Next Paint : réactivité aux interactions |
| CLS | ≤ 0,1 | Cumulative Layout Shift : stabilité visuelle |
8.2 Outils
- Lighthouse (DevTools → Audits) : score PWA + Performance + Accessibility + Best Practices + SEO.
- PageSpeed Insights : mesure terrain (CrUX).
- Web Vitals JS :
web-vitalspour instrumenter en production.
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
8.3 Optimisations clés
- Precache des coquilles d'application (app shell) au
install. - Preload des routes critiques (
<link rel="preload">). - Lazy-load des images et sections hors viewport.
- Compresser (Brotli/gzip) et servir avec
Cache-Controladapté. - Éviter le layout shift : dimensions explicites des images, font-display
swap.
9. Comparaison PWA / natif / RN / Flutter
9.1 Tableau comparatif
| Critère | PWA | Natif (Kotlin/Swift) | React Native | Flutter |
|---|---|---|---|---|
| Code partagé | 1 (web) | 2 (platformes) | JS | Dart |
| Store | Facultatif | Obligatoire | Obligatoire | Obligatoire |
| Mise à jour | Instantanée | Revue (hours/jours) | Revue | Revue |
| Offline | SW + Cache API | Local natif | AsyncStorage/MMKV | Hive/SQLite |
| Notifications push | Web Push (VAPID) | FCM/APNs | FCM | FCM |
| Accès matériel | Limité (permissions) | Complet | Bridges | Plugins |
| Performance | Bonne (pas native) | Optimale | Bonne | Optimale |
| Coût | Faible | Élevé | Moyen | Moyen |
| Store revenue/features | Restreintes (pas d'IAP natif simple) | Complètes | Complètes | Complètes |
9.2 Quand choisir quoi ?
| Situation | Choix |
|---|---|
| Landing + offline léger, coût minimal | PWA |
| App fullscreen, appareils photo/santé, IAP | Natif |
| Équipe JS existante, time-to-market | React Native |
| UI soignée, performances graphiques | Flutter |
| Besoin offline + installable + budget faible | PWA d'abord, natif enveloppé (TWA) |
9.3 Hybridation
- Trusted Web Activity (TWA) : PWA enveloppée dans une Activity Android (playStoreUrl, dynamic color).
- Tout PWA n'est pas une app store-ready : l'App Store interdit la simple duplication web.
10. Sécurité d'une PWA
10.1 HTTPS et HTTPS-only
- HTTPS obligatoire pour SW, manifest, push et toutes les capacités.
- Dégradation explicite : activer HTTPS-only (Firefox), et servir un message clair si
http:.
10.2 Permissions et minimisation
- Demander les permissions au moment du besoin (pas au chargement).
Permissions APIpour connaître l'état sans re-demander :
const s = await navigator.permissions.query({ name: 'notifications' });
// 'granted' | 'denied' | 'prompt'
10.3 Isolation et sanitization
- CSP stricte (SW inclus) : pas de
eval, pas d'inline non hashé. - Sanitiser tout contenu user (XSS) :
textContent, DOMPurify. - Ne jamais stocker de secrets côté client ; IndexedDB et Cache API ne sont pas chiffrés.
10.4 Surface d'attaque du Service Worker
- Un XSS peut enregistrer un SW malveillant : c'est une menace persistante.
- Protections :
Content-Security-Policy+ cookieshttpOnly/SameSite+ isolationCOOP/COEP(Cross-Origin Isolated) pour les capacités avancées (SharedArrayBuffer).
Synthèse
- Une PWA = HTTPS + Manifest + Service Worker : installable, offline, engageante.
- Le Service Worker est un proxy réseau éphémère :
install(pré-cache),activate(nettoyage),fetch(stratégie),push/sync. - Stratégies de cache : network-first pour les pages, cache-first pour les assets, stale-while-revalidate pour les données.
- IndexedDB stocke les données métier ; Background Sync rejoue les actions hors ligne.
- beforeinstallprompt offre une UX d'installation contrôlée ; iOS passe par le menu Partager.
- Lighthouse et les Core Web Vitals (LCP, INP, CLS) mesurent l'expérience réelle.
- PWA ≠ natif : comparer coût, performance, accès matériel et revenus avant de choisir.
- La sécurité repose sur HTTPS, permissions minimales, CSP stricte et attention au risque SW après XSS.