MFormations
Modern Mobile Engineering

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

  1. Qu'est-ce qu'une PWA ?
  2. Web App Manifest
  3. Service Workers
  4. Stratégies de cache
  5. Offline : Cache API, IndexedDB, Background Sync
  6. Notifications push
  7. Installabilité (A2HS)
  8. Performance : Core Web Vitals et Lighthouse
  9. Comparaison PWA / natif / RN / Flutter
  10. 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
Vibrationnavigator.vibrate()
Géolocalisationnavigator.geolocation
Contactsnavigator.contacts
FichiersshowOpenFilePicker
Presse-papiersnavigator.clipboard
Badgenavigator.setAppBadge()
Web Sharenavigator.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-icon pour 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énementRôle
installPré-cache des ressources critiques (une seule fois)
activateNettoyage des caches obsolètes, contrôle de la page
fetchStratégie de cache pour chaque requête
messageCommunication avec la page
pushRéception d'une notification
syncBackground 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égieUsageDisponibilitéFraîcheur
Cache-firstAssets statiques (images, CSS)ImmédiateStale
Network-firstPages HTML, API critiquesSi réseau OKFraîche
Stale-while-revalidateDonnées faiblement volatilesImmédiateMise à jour différée
Network-onlyPOST, paiementRéseau requisToujours 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

APITypeUsage
Cache APIRequêtes/réponses HTTPAssets, pages, réponses d'API
IndexedDBBase NoSQL côté clientDonné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: true est 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-icon et <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étriqueSeuil "bon"Description
LCP≤ 2,5 sLargest Contentful Paint : contenu principal
INP≤ 200 msInteraction to Next Paint : réactivité aux interactions
CLS≤ 0,1Cumulative 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-vitals pour 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-Control adapté.
  • Éviter le layout shift : dimensions explicites des images, font-display swap.

9. Comparaison PWA / natif / RN / Flutter

9.1 Tableau comparatif

CritèrePWANatif (Kotlin/Swift)React NativeFlutter
Code partagé1 (web)2 (platformes)JSDart
StoreFacultatifObligatoireObligatoireObligatoire
Mise à jourInstantanéeRevue (hours/jours)RevueRevue
OfflineSW + Cache APILocal natifAsyncStorage/MMKVHive/SQLite
Notifications pushWeb Push (VAPID)FCM/APNsFCMFCM
Accès matérielLimité (permissions)CompletBridgesPlugins
PerformanceBonne (pas native)OptimaleBonneOptimale
CoûtFaibleÉlevéMoyenMoyen
Store revenue/featuresRestreintes (pas d'IAP natif simple)ComplètesComplètesComplètes

9.2 Quand choisir quoi ?

SituationChoix
Landing + offline léger, coût minimalPWA
App fullscreen, appareils photo/santé, IAPNatif
Équipe JS existante, time-to-marketReact Native
UI soignée, performances graphiquesFlutter
Besoin offline + installable + budget faiblePWA 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 API pour 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 + cookies httpOnly/SameSite + isolation COOP/COEP (Cross-Origin Isolated) pour les capacités avancées (SharedArrayBuffer).

Synthèse

  1. Une PWA = HTTPS + Manifest + Service Worker : installable, offline, engageante.
  2. Le Service Worker est un proxy réseau éphémère : install (pré-cache), activate (nettoyage), fetch (stratégie), push/sync.
  3. Stratégies de cache : network-first pour les pages, cache-first pour les assets, stale-while-revalidate pour les données.
  4. IndexedDB stocke les données métier ; Background Sync rejoue les actions hors ligne.
  5. beforeinstallprompt offre une UX d'installation contrôlée ; iOS passe par le menu Partager.
  6. Lighthouse et les Core Web Vitals (LCP, INP, CLS) mesurent l'expérience réelle.
  7. PWA ≠ natif : comparer coût, performance, accès matériel et revenus avant de choisir.
  8. La sécurité repose sur HTTPS, permissions minimales, CSP stricte et attention au risque SW après XSS.