W3docs

Service Workers

Impara i Service Workers JavaScript: ciclo di vita, registrazione, strategie di caching, supporto offline e aggiornamenti con versioning della cache.

Service Workers: Creare Potenti Applicazioni Web Offline-First

Un Service Worker è uno script che il browser esegue in background, separato dalla tua pagina web, senza accesso diretto al DOM. Si interpone tra la tua applicazione web e la rete come proxy programmabile: ogni richiesta effettuata dalla pagina può essere intercettata, ispezionata, servita dalla cache o riscritta prima che raggiunga il server.

Questa singola capacità sblocca le funzionalità che gli utenti ora si aspettano dalle moderne app web: funzionamento offline, caricamenti ripetuti quasi istantanei, sincronizzazione dati in background e notifiche push. I Service Workers sono il motore alla base delle Progressive Web Apps (PWA).

Questo capitolo spiega cosa sono i Service Workers, il ciclo di vita che attraversano, come registrarne uno, le strategie di caching più comuni e come distribuire aggiornamenti senza servire file obsoleti. L'API Service Worker è interamente basata su Promise, quindi una buona comprensione di async/await e della Fetch API sarà utile.

Cos'è un Service Worker?

Un Service Worker è un tipo di web worker: un file JavaScript che gira sul proprio thread, indipendente dalla pagina che lo ha registrato. Poiché gira fuori dal thread principale, non può bloccare la tua UI, ma non può nemmeno accedere al DOM — comunica con le pagine tramite eventi e messaggi.

Caratteristiche chiave che lo distinguono da uno script di pagina normale:

  • È guidato dagli eventi. Il browser lo avvia quando c'è lavoro da fare (un fetch in arrivo, un push, un sync) e può terminarlo quando è inattivo. Non assumere mai che lo stato globale sopravviva tra un evento e l'altro.
  • Ha un ciclo di vita. Un Service Worker viene installato, attivato, e solo allora controlla le pagine. Gli aggiornamenti seguono regole rigide in modo che gli utenti non vengano mai serviti con un'app aggiornata a metà.
  • Ha uno scope. Un worker può intercettare solo le richieste che rientrano nel suo scope — di default la directory in cui si trova lo script.
  • Richiede un contesto sicuro. I Service Workers funzionano solo su HTTPS (o localhost durante lo sviluppo), perché uno script capace di riscrivere ogni risposta è una superficie di attacco seria.

Perché usare i Service Workers?

BeneficioCosa ti offre
Supporto offlineMetti in cache l'app shell e le risorse critiche in modo che l'app si carichi senza connessione di rete.
PrestazioniLe visite ripetute vengono servite dalla cache locale, eliminando i round trip e riducendo i tempi di caricamento.
Sincronizzazione in backgroundDifferisci le richieste fallite (es. un commento inviato) e ritentale automaticamente al ripristino della connettività.
Notifiche pushRicevi e visualizza messaggi da un server anche quando non è aperta alcuna scheda.
Controllo completo delle richiesteDecidi per ogni richiesta se usare la cache, la rete o una logica personalizzata.

Il ciclo di vita del Service Worker

Un Service Worker attraversa una serie di stati ben definiti. Comprenderli è la cosa più importante per evitare bug del tipo "perché sta ancora girando il mio vecchio codice?".

  1. Register — la pagina chiama navigator.serviceWorker.register(). Il browser scarica lo script.
  2. Install — l'evento install si attiva una volta per versione del worker. Qui si pre-cachano i file di cui l'app ha bisogno per funzionare offline.
  3. Wait — se un worker precedente controlla ancora pagine aperte, il nuovo worker aspetta. Non si attiverà finché ogni pagina controllata non sarà chiusa, a meno che non si chiami self.skipWaiting().
  4. Activate — l'evento activate si attiva. Qui si ripuliscono le cache delle versioni precedenti.
  5. Control / Fetch — una volta attivo, il worker intercetta gli eventi fetch per le pagine nel suo scope.
register → install → (waiting) → activate → fetch / push / sync ...

Due metodi guidano questo flusso:

  • self.skipWaiting() (in install) dice al nuovo worker di attivarsi immediatamente invece di aspettare.
  • self.clients.claim() (in activate) consente al worker attivo di prendere il controllo delle pagine già aperte, invece di controllare solo le pagine caricate dopo l'attivazione.

Perché esiste la fase di attesa: garantisce che una singola versione del codice controlli una pagina per tutta la sua durata, così da non mescolare mai HTML vecchio con script appena messi in cache. Usa skipWaiting() con consapevolezza, perché può sostituire il worker di controllo mentre un utente è attivo.

Vincoli da tenere a mente

  • Solo HTTPS o localhost. Le pagine non sicure non possono registrare un worker.
  • Lo scope limita l'intercettazione. Un worker su /app/sw.js controlla /app/ e le sue sottodirectory — non l'intera origine. Posiziona lo script alla radice del sito per controllare tutto.
  • Nessun accesso al DOM. Aggiorna la pagina inviando messaggi o facendo leggere dalla cache alla pagina stessa.
  • Il worker può essere terminato in qualsiasi momento. Archivia tutto ciò che deve persistere nella Cache Storage, IndexedDB, o Web Storage — non nelle variabili globali del worker.

Passo 1 — Registrare il Service Worker

Nel JavaScript della tua pagina, registra lo script con navigator.serviceWorker.register(). Rileva sempre la funzionalità prima, e registra dopo il caricamento della pagina in modo che il worker non sia in competizione con il primo rendering:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js')
      .then((registration) => {
        console.log('Service Worker registered, scope:', registration.scope);
      })
      .catch((error) => {
        console.error('Service Worker registration failed:', error);
      });
  });
}

La chiamata register() restituisce una Promise che si risolve con una ServiceWorkerRegistration. Il suo scope indica quali URL controlla questo worker.

Passo 2 — Scrivere lo script del Service Worker

Crea un file separato (qui, sw.js) per il worker stesso. Al suo interno gestisci gli eventi del ciclo di vita e decidi come vengono servite le richieste. L'esempio seguente pre-cacha un'app shell durante l'installazione, pulisce le cache obsolete durante l'attivazione, e serve una strategia cache-first con un fallback offline:

const CACHE_VERSION = 'v1';
const PRECACHE_URLS = ['/', '/index.html', '/styles.css', '/offline.html'];

// Install: pre-cache the app shell.
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_VERSION).then((cache) => cache.addAll(PRECACHE_URLS))
  );
  self.skipWaiting(); // activate this version immediately
});

// Activate: remove caches from previous versions.
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches
      .keys()
      .then((keys) =>
        Promise.all(
          keys
            .filter((key) => key !== CACHE_VERSION)
            .map((key) => caches.delete(key))
        )
      )
      .then(() => self.clients.claim()) // take control of open pages
  );
});

// Fetch: serve from cache, fall back to network, then to the offline page.
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return (
        cached ||
        fetch(event.request).catch(() => caches.match('/offline.html'))
      );
    })
  );
});

Alcune cose degne di nota:

  • event.waitUntil(promise) mantiene il worker attivo fino al completamento della Promise, in modo che il browser non lo termini a metà installazione o a metà attivazione.
  • event.respondWith(promise) è il modo in cui rispondi a un evento fetch — restituisci una Response (dalla cache) o una Promise che si risolve in una.
  • self.skipWaiting() forza la nuova versione ad attivarsi senza attendere la chiusura delle pagine vecchie. Combinato con clients.claim(), il nuovo worker subentra immediatamente. Comodo in sviluppo; usalo con attenzione in produzione, perché sostituire il worker di controllo a metà sessione può interrompere gli utenti attivi.

Passo 3 — Il worker prende il controllo

Dopo il completamento di install e activate, il worker controlla le pagine nel suo scope e il suo handler fetch intercetta le loro richieste. Nota che il primo caricamento di una pagina non passa attraverso il worker — il worker si sta installando durante quel caricamento. Dal secondo caricamento in poi, le richieste fluiscono attraverso il tuo handler fetch.

Strategie di caching comuni

Non esiste un'unica strategia di caching "giusta" — ne scegli una per tipo di risorsa in base a quanto i dati devono essere aggiornati.

StrategiaCome funzionaIdeale per
Cache firstRestituisce la copia in cache; ricorre alla rete solo in caso di mancanza.Risorse statiche che cambiano raramente (CSS, font, l'app shell).
Network firstProva la rete; ricade sulla cache in caso di fallimento.Contenuti aggiornati frequentemente (risposte API, feed di notizie).
Stale-while-revalidateServe immediatamente la copia in cache, poi recupera una copia aggiornata in background per la prossima volta.Risorse in cui la velocità conta più della freschezza perfetta (avatar, miniature).

Un handler network-first appare così:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // Cache a copy for offline use, then return the fresh response.
        const copy = response.clone();
        caches.open('v1').then((cache) => cache.put(event.request, copy));
        return response;
      })
      .catch(() => caches.match(event.request))
  );
});

Suggerimento: Il corpo di una Response può essere letto una sola volta, ecco perché devi fare il clone() prima sia di metterla in cache che di restituirla.

Aggiornare un Service Worker

Quando cambi sw.js, il browser rileva la differenza di byte, scarica il nuovo file ed esegue il suo evento install. Il nuovo worker aspetta poi (a meno che tu non chiami skipWaiting()). Il pattern di versioning della cache sopra descritto è ciò che mantiene puliti gli aggiornamenti:

  1. Incrementa CACHE_VERSION (es. 'v1''v2') ogni volta che le risorse in cache cambiano.
  2. Il nuovo install scrive le risorse nella nuova cache.
  3. Il nuovo activate elimina ogni cache la cui chiave non corrisponde alla versione corrente, rimuovendo i file obsoleti.

Questo garantisce che gli utenti non ricevano mai un mix di risorse vecchie e nuove dopo un deploy.

Esempio Reale: Notifiche sullo Stato di Connettività

Questo esempio illustra una funzionalità comunemente usata in molti siti web e applicazioni moderne, come i servizi di streaming quali Netflix o le app basate su cloud come Google Docs, per informare gli utenti sul loro stato di connettività. Notificando agli utenti quando sono offline, queste piattaforme migliorano l'esperienza utente assicurando che siano consapevoli di potenziali problemi con la sincronizzazione dei dati o le interruzioni dello streaming. Questo esempio si concentra sull'integrazione UI nel thread principale, mentre lo script del Service Worker rimane lo stesso dell'esempio precedente.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Connectivity Notifier</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        text-align: center;
        margin-top: 50px;
      }
      #status {
        padding: 10px;
        border-radius: 5px;
        color: #fff;
        font-size: 24px;
      }
      .online {
        background-color: #4caf50;
        animation: blinker 1s linear infinite;
      }
      .offline {
        background-color: #f44336;
        animation: blinker 1s linear infinite;
      }
      @keyframes blinker {
        50% {
          opacity: 0.5;
        }
      }
    </style>
  </head>
  <body>
    <h1>Connectivity Notifier</h1>
    <p id="status" class="offline">Checking connectivity...</p>

    <script>
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker.register("sw.js").then(function () {
          console.log("Service Worker Registered");
        });

        window.addEventListener('online', () => {
          const statusElement = document.getElementById("status");
          statusElement.textContent = "Online";
          statusElement.className = "online";
        });

        window.addEventListener('offline', () => {
          const statusElement = document.getElementById("status");
          statusElement.textContent = "Offline";
          statusElement.className = "offline";
        });
      }
    </script>
  </body>
</html>

Spiegazione:

  • Controllo della connettività: La pagina principale ascolta gli eventi online e offline sull'oggetto window e aggiorna l'UI immediatamente, evitando l'approccio inaffidabile del polling.
  • Feedback all'utente: La pagina mostra lo stato di connettività attuale, aiutando gli utenti a capire come integrare le funzionalità in background con un'interfaccia reattiva.
  • Pulizia del codice: Il listener navigator.serviceWorker.onmessage inutilizzato è stato rimosso, poiché lo script del Service Worker non invia alcun messaggio.

Conclusione

I Service Workers trasformano il browser in un proxy di rete programmabile, rendendo possibile la creazione di app veloci, resilienti e utilizzabili offline. Le chiavi sono comprendere il ciclo di vita (install → wait → activate → fetch), scegliere una strategia di caching adatta a ciascuna risorsa e usare il versioning della cache affinché gli aggiornamenti vengano distribuiti in modo pulito.

Per approfondire i componenti fondamentali su cui si basano i Service Workers, consulta:

Pratica

Pratica
Quali sono le caratteristiche e le funzionalità principali dei Service Workers JavaScript?
Quali sono le caratteristiche e le funzionalità principali dei Service Workers JavaScript?
Was this page helpful?