Parliamone
// tecnologie.progressive-web-apps

Progressive Web Apps (PWA)

Service worker, web app manifest, strategie di caching e notifiche push: architettura, compromessi implementativi e limiti di un modello applicativo che estende le capacità del browser verso l'esperienza nativa.

E-commerce & WebSoftware Architecture

Executive summary

Le applicazioni web tradizionali dipendono dalla connessione di rete per funzionare e non possono inviare aggiornamenti all'utente quando il browser è chiuso, due vincoli che le separano nettamente dalle applicazioni installate sul dispositivo. Questo articolo analizza un insieme di tecnologie che consente di superare tali limiti, permettendo a un sito web di funzionare anche senza connessione, di essere installato sulla schermata principale del dispositivo e di ricevere messaggi dal server in tempo reale. L'analisi evidenzia che l'efficacia di queste soluzioni dipende dalla scelta accurata delle strategie con cui i contenuti vengono conservati localmente, e che il supporto da parte dei diversi sistemi operativi e browser rimane disomogeneo, con restrizioni significative nell'ecosistema di alcuni produttori. I risultati concreti ottenuti da grandi piattaforme, riduzione dei tempi di caricamento, aumento del coinvolgimento degli utenti e diminuzione dei dati consumati, dimostrano tuttavia che il modello è maturo per applicazioni in produzione, a condizione che se ne comprendano i vincoli.


Background

Il concetto di Progressive Web App è stato formalizzato nel 2015 da Alex Russell e Frances Berriman per descrivere una nuova classe di applicazioni web che, sfruttando funzionalità introdotte nei browser moderni, acquisiscono progressivamente caratteristiche tipiche delle applicazioni native [1]. La definizione originale identifica proprietà quali l'indipendenza dalla connettività, l'installabilità, la freschezza automatica dei contenuti e la capacità di inviare notifiche, tutte abilitate da un insieme di API standardizzate dal W3C e implementate nei motori di rendering dei principali browser.

L'architettura di una PWA si fonda su tre pilastri tecnologici distinti ma interconnessi. Il primo è il service worker, un web worker specializzato che opera come proxy di rete programmabile tra l'applicazione e il server, intercettando le richieste HTTP e consentendo risposte da cache locale quando la rete non è disponibile [2, 3]. Il secondo è il web app manifest, un file JSON che dichiara i metadati dell'applicazione, nome, icone, colori, modalità di visualizzazione, e che il browser utilizza per determinare se l'applicazione soddisfa i criteri di installabilità [4]. Il terzo è l'insieme delle API di caching e storage (Cache Storage API, IndexedDB) che forniscono i meccanismi di persistenza locale su cui le strategie offline-first si appoggiano [5].

La motivazione tecnica alla base delle PWA risiede nella convergenza di due fenomeni. Da un lato, l'evoluzione delle API web ha progressivamente colmato il divario funzionale con le piattaforme native, un processo accelerato dal progetto Chromium noto come Project Fugu, che ha introdotto oltre cinquanta nuove API tra cui File System Access, Web Bluetooth e Web Share [6]. Dall'altro, i costi di sviluppo e distribuzione di applicazioni native per piattaforme multiple hanno spinto verso modelli cross-platform, e le PWA rappresentano l'alternativa con il minor costo di distribuzione: nessun processo di approvazione da parte degli store, aggiornamenti istantanei e un singolo codebase [7]. Biørn-Hansen, Majchrzak e Grønli hanno dimostrato empiricamente che le PWA raggiungono prestazioni paragonabili alle implementazioni native nella maggior parte degli scenari, risultando inferiori solo nel tempo di avvio su iOS [7].

La domanda centrale di questo articolo è: come si progetta un'applicazione web progressiva che funzioni in modo affidabile offline, sfrutti strategie di caching ottimali e integri notifiche push, e quali sono i compromessi architetturali e i limiti delle tecnologie coinvolte?


Architettura dei service worker

Il modello di esecuzione

Un service worker è uno script JavaScript che il browser esegue in un thread separato rispetto alla pagina web, senza accesso diretto al DOM [2, 3]. Questa separazione non è una limitazione accidentale ma una scelta architetturale deliberata: il service worker deve poter operare anche quando nessuna pagina dell'applicazione è aperta, condizione necessaria per gestire notifiche push e sincronizzazione in background. La comunicazione con le pagine controllate avviene attraverso l'API postMessage e il canale MessageChannel, mantenendo un disaccoppiamento che garantisce la non-interferenza con il thread di rendering.

Il service worker si registra su un ambito (scope) definito dall'URL del file che lo contiene. Lo scope determina quali richieste il service worker può intercettare: un service worker registrato su /app/sw.js controllerà le navigazioni e le risorse sotto il percorso /app/, ma non potrà intercettare richieste dirette a /api/ se questo percorso è esterno allo scope. Questa delimitazione è un meccanismo di sicurezza che impedisce a un service worker di interferire con risorse che non gli competono [3]. L'intero modello richiede inoltre un'origine sicura (HTTPS o localhost), poiché un proxy di rete programmabile in un contesto non cifrato rappresenterebbe un vettore di attacco per il man-in-the-middle.

Ciclo di vita

Il ciclo di vita del service worker è il meccanismo più critico da comprendere per un'implementazione corretta, poiché determina quando il codice di caching viene eseguito, quando le cache obsolete vengono ripulite e quando un aggiornamento entra in vigore. Il ciclo si articola in tre fasi principali, ciascuna associata a un evento specifico [3, 8].

Fase di installazione (install). Quando il browser scarica un file service worker nuovo o modificato (il confronto avviene byte per byte), attiva l'evento install. Questa fase è il momento designato per il precaching: le risorse critiche dell'applicazione, shell HTML, fogli di stile, script principali, font, vengono scaricate e inserite nella Cache Storage API tramite cache.addAll(). L'operazione è atomica rispetto alla fase: se anche una sola risorsa fallisce il download, l'intera installazione fallisce e il service worker rimane nello stato redundant [8]. Questa garanzia di atomicità è fondamentale per assicurare che l'applicazione disponga di un set completo e coerente di risorse per il funzionamento offline.

Fase di attesa e attivazione (activate). Un service worker appena installato non prende immediatamente il controllo delle pagine: entra in uno stato di attesa finché tutte le pagine controllate dalla versione precedente non vengono chiuse. Questo comportamento, apparentemente controintuitivo, previene una classe di bug in cui risorse cached dalla versione precedente vengono servite da logica della versione nuova, o viceversa [8]. L'evento activate è il momento appropriato per la pulizia delle cache obsolete: si enumerano le cache esistenti con caches.keys() e si eliminano quelle il cui nome non corrisponde alla versione corrente. Il metodo self.skipWaiting(), invocabile nell'evento install, forza il passaggio immediato alla fase di attivazione, mentre clients.claim() nell'evento activate consente al service worker di prendere il controllo delle pagine già aperte senza attendere un nuovo caricamento.

Intercettazione delle richieste (fetch). Una volta attivo e in controllo, il service worker riceve un evento fetch per ogni richiesta di rete originata dalle pagine nel suo scope [2, 3]. L'handler di questo evento è il punto in cui si implementano le strategie di caching: in base al tipo di risorsa, all'URL o ad altri criteri, il service worker può rispondere dalla cache, dalla rete o con una combinazione delle due. L'oggetto FetchEvent espone il metodo respondWith(), che accetta una Promise<Response> e la utilizza come risposta alla richiesta, sostituendo completamente il comportamento di rete predefinito del browser.

Aggiornamento e versionamento

Il browser verifica la presenza di aggiornamenti al service worker ad ogni navigazione verso una pagina nello scope, con una frequenza massima di un controllo ogni 24 ore anche se la pagina è rimasta aperta [8]. Se il file scaricato differisce dal service worker corrente per almeno un byte, il browser avvia il processo di installazione della nuova versione. La coesistenza di due versioni, quella attiva e quella in attesa, è un aspetto che richiede attenzione nella gestione delle cache: è prassi consolidata utilizzare nomi di cache versionati (ad esempio static-v2, dynamic-v2) e implementare la pulizia delle versioni precedenti nell'evento activate.

Un pattern particolarmente rilevante per le applicazioni in produzione è il Navigation Preload, che consente di avviare la richiesta di rete in parallelo all'avvio del service worker, eliminando il ritardo introdotto dal cold start del worker stesso [8]. Senza Navigation Preload, una navigazione che passa attraverso un service worker appena risvegliato subisce una latenza aggiuntiva pari al tempo di avvio del worker, che può raggiungere centinaia di millisecondi su dispositivi mobili di fascia bassa.


Web app manifest e installabilità

Struttura e proprietà del manifest

Il web app manifest è un file JSON, tipicamente denominato manifest.json o manifest.webmanifest, collegato alle pagine HTML dell'applicazione tramite un elemento <link rel="manifest"> [4, 9]. La specifica W3C definisce un insieme di proprietà che descrivono l'identità e il comportamento dell'applicazione quando installata sul dispositivo dell'utente. Le proprietà fondamentali includono name e short_name (il nome completo e la versione abbreviata per contesti con spazio limitato), icons (un array di oggetti con src, sizes e type per le icone a diverse risoluzioni), start_url (l'URL di ingresso dell'applicazione), display (la modalità di visualizzazione) e theme_color e background_color (i colori dell'interfaccia di sistema).

La proprietà display merita un'analisi specifica poiché determina il grado di integrazione visiva con il sistema operativo. I valori possibili formano una gerarchia: fullscreen occupa l'intero schermo senza alcun elemento dell'interfaccia del browser, standalone presenta l'applicazione con una barra del titolo del sistema operativo ma senza controlli del browser, minimal-ui aggiunge un set ridotto di controlli di navigazione, e browser apre l'applicazione in una scheda convenzionale [4, 9]. La scelta influenza direttamente la percezione dell'utente: un'applicazione in modalità standalone è indistinguibile da un'applicazione nativa per quanto riguarda la presenza nella barra delle applicazioni e nel task switcher del sistema operativo.

Criteri di installabilità

I criteri che un browser utilizza per determinare se un'applicazione web è installabile variano tra implementazioni, ma convergono su un nucleo comune. Chromium richiede che l'applicazione sia servita su HTTPS, disponga di un manifest con almeno name, icons (con un'icona di almeno 144x144 pixel in formato PNG) e start_url, e abbia un service worker registrato con un handler per l'evento fetch [9, 10]. Safari su iOS non implementa un prompt di installazione automatico: l'utente deve utilizzare manualmente l'opzione "Aggiungi alla schermata Home" dal menu di condivisione, una differenza nell'esperienza utente che riduce significativamente il tasso di installazione sulla piattaforma Apple [11].

L'evento beforeinstallprompt, disponibile in Chromium, consente all'applicazione di intercettare e posticipare il prompt di installazione nativo del browser, presentandolo in un momento contestualmente più appropriato, ad esempio dopo che l'utente ha completato un'azione significativa nell'applicazione [10]. Questa possibilità di controllo sul timing del prompt ha dimostrato di aumentare il tasso di conversione dell'installazione, poiché l'utente ha già sperimentato il valore dell'applicazione prima di ricevere la proposta.

Scope e navigazione

Il manifest definisce anche il campo scope, che delimita l'insieme di URL che il browser considera parte dell'applicazione installata. Le navigazioni verso URL esterni allo scope vengono aperte nel browser predefinito del sistema operativo anziché all'interno della finestra dell'applicazione. Questa semantica è fondamentale per le applicazioni che integrano flussi di autenticazione OAuth o pagamenti tramite gateway esterni: se l'URL del provider è esterno allo scope, la navigazione interrompe l'esperienza applicativa. La pianificazione dello scope richiede pertanto un'analisi preventiva di tutti i flussi di navigazione dell'applicazione, inclusi quelli gestiti da servizi di terze parti.


Strategie di caching e pattern offline-first

La Cache Storage API

La Cache Storage API fornisce un meccanismo di persistenza chiave-valore in cui le chiavi sono oggetti Request e i valori sono oggetti Response [5]. A differenza della cache HTTP del browser, che opera secondo le direttive dei response header e non è controllabile programmaticamente, la Cache Storage API è interamente gestita dal codice dell'applicazione: le risposte non scadono automaticamente, non vengono invalidate sulla base di header Cache-Control e non vengono eliminate dal browser se non esplicitamente (o in caso di pressione sulla quota di storage). Un'origine può creare multiple cache con nomi distinti attraverso caches.open(name), e ciascuna cache può contenere risposte per qualsiasi URL, non solo per risorse della stessa origine.

È essenziale distinguere la Cache Storage API da IndexedDB, poiché i due meccanismi rispondono a esigenze diverse. La Cache Storage API è ottimizzata per la memorizzazione di risposte HTTP (HTML, CSS, JavaScript, immagini, font), mentre IndexedDB è un database a oggetti progettato per dati strutturati dell'applicazione (profili utente, contenuti generati, code di sincronizzazione) [5]. Un'architettura offline-first matura utilizza entrambi: la Cache Storage API per le risorse statiche e le risposte API, IndexedDB per lo stato applicativo e i dati in attesa di sincronizzazione.

Strategie di caching

La scelta della strategia di caching per ciascun tipo di risorsa è una delle decisioni architetturali più impattanti nella progettazione di una PWA, poiché determina il compromesso tra freschezza dei contenuti, velocità di risposta e resilienza offline [12, 13].

Cache First (Cache Falling Back to Network). Il service worker verifica innanzitutto se la risorsa è presente in cache; in caso affermativo, la restituisce immediatamente senza contattare la rete. Solo in assenza di un match nella cache la richiesta viene inoltrata al server, e la risposta viene memorizzata per utilizzi futuri. Questa strategia è ottimale per risorse che cambiano raramente e per le quali la velocità di risposta è prioritaria rispetto alla freschezza: font, icone, librerie JavaScript versionati, immagini di prodotto con URL immutabili [12]. Il rischio principale è servire contenuto obsoleto se la strategia di invalidazione della cache non è adeguata.

Network First (Network Falling Back to Cache). La richiesta viene inoltrata alla rete; se la risposta ha successo, viene memorizzata in cache e restituita. Se la rete non è raggiungibile, il service worker risponde dalla cache. Questa strategia privilegia la freschezza ed è appropriata per contenuti che cambiano frequentemente e per i quali il dato aggiornato è prioritario: risposte API, pagine HTML con contenuto dinamico, feed di notizie [12]. Il costo è una latenza percepita superiore rispetto a Cache First, poiché ogni richiesta attende la risposta di rete (o il timeout) prima di consultare la cache.

Stale-While-Revalidate. Il service worker risponde immediatamente dalla cache (se disponibile), ma contemporaneamente avvia una richiesta di rete per aggiornare la cache in background. La richiesta successiva riceverà la versione aggiornata. Questa strategia bilancia velocità e freschezza, ed è particolarmente efficace per risorse che cambiano periodicamente ma per le quali una versione leggermente obsoleta è accettabile: avatar utente, contenuti editoriali, dati di catalogo [12, 13]. Il compromesso è che l'utente visualizza sempre una versione "indietro di un aggiornamento" rispetto al server, il che può risultare problematico per contenuti la cui coerenza temporale è critica.

Cache Only e Network Only. Le due strategie estreme: Cache Only non consulta mai la rete ed è utilizzata esclusivamente per risorse precached che non cambiano tra un deployment e il successivo; Network Only bypassa completamente la cache ed è appropriata per richieste che non hanno senso offline, come le chiamate di analytics o le operazioni di scrittura [12].

Il pattern App Shell

Il pattern App Shell separa l'interfaccia utente in due componenti: la shell, l'HTML strutturale, i fogli di stile e lo script minimo necessario per renderizzare il layout dell'applicazione, e il contenuto dinamico, caricato successivamente dalla rete o da IndexedDB [14]. La shell viene precached durante l'installazione del service worker e servita con strategia Cache First, garantendo un caricamento istantaneo anche in assenza di rete. Il contenuto dinamico viene poi inserito tramite richieste API gestite con strategie Network First o Stale-While-Revalidate.

Questo pattern è efficace perché sfrutta la composizione: la shell, rappresentando tipicamente meno di 50 KB compressi, si carica in tempi dell'ordine delle decine di millisecondi dalla cache, fornendo un feedback visivo immediato all'utente. Il contenuto dinamico viene poi iniettato progressivamente, replicando il pattern di caricamento percepito delle applicazioni native. Twitter Lite ha adottato questo approccio ottenendo un bundle PWA di 600 KB rispetto ai 23.5 MB dell'applicazione nativa Android, con un incremento del 65% nelle pagine per sessione e una riduzione del 70% nel consumo di dati [15].

Sincronizzazione in background

La Background Sync API estende il modello offline-first alle operazioni di scrittura [16]. Quando l'utente compie un'azione che richiede una richiesta di rete (invio di un messaggio, salvataggio di un form, upload di un file) e la rete non è disponibile, l'applicazione memorizza l'operazione in IndexedDB e registra un evento di sincronizzazione con un tag identificativo tramite registration.sync.register('sync-tag'). Quando la connettività viene ripristinata, anche se l'utente ha chiuso il browser, il service worker riceve un evento sync e può eseguire le operazioni in coda.

Il meccanismo implementa un retry con backoff esponenziale: se l'handler dell'evento sync genera un errore, il browser riprogramma il tentativo con un intervallo crescente. Questa resilienza è fondamentale per applicazioni in contesti con connettività intermittente, come le applicazioni field service o le applicazioni per ambienti industriali. Una limitazione significativa è che la Background Sync API non è supportata su Safari/WebKit, il che richiede un fallback esplicito su piattaforme Apple [11].


Push Notifications: protocollo e architettura

Lo stack protocollare

L'architettura delle push notification web si basa su tre specifiche complementari: la Push API (W3C), il protocollo Web Push (RFC 8030) e il meccanismo di identificazione VAPID (RFC 8292) [17, 18, 19]. La comprensione dell'interazione tra questi tre livelli è necessaria per un'implementazione corretta e sicura.

La Push API, definita dal W3C, espone l'interfaccia PushManager attraverso la quale l'applicazione può richiedere una sottoscrizione push all'utente [17]. L'invocazione di pushManager.subscribe() genera una PushSubscription contenente un endpoint URL unico (gestito dal push service del browser vendor) e le chiavi crittografiche necessarie per cifrare i messaggi. L'endpoint è specifico per la combinazione browser-dispositivo-service worker e rappresenta il canale attraverso cui il server applicativo invierà i messaggi.

RFC 8030 definisce il protocollo di trasporto tra il server applicativo e il push service del browser [18]. Il server applicativo invia una richiesta HTTP POST all'endpoint contenuto nella PushSubscription, con il payload cifrato secondo lo schema descritto in RFC 8291 (Message Encryption for Web Push). Il push service, gestito da Google (FCM) per Chrome, da Mozilla per Firefox, da Apple (APNs) per Safari, riceve il messaggio, lo conserva fino a quando il dispositivo dell'utente è raggiungibile e lo consegna al service worker appropriato.

VAPID (Voluntary Application Server Identification), definito in RFC 8292, consente al server applicativo di identificarsi presso il push service attraverso un token JWT firmato con una coppia di chiavi ECDSA [19]. Questo meccanismo risolve due problemi: permette al push service di attribuire le richieste a un'entità specifica (utile per il rate limiting e la risoluzione di abusi), e consente al browser di restringere una sottoscrizione a un singolo server applicativo, impedendo che un endpoint venga utilizzato da terzi non autorizzati.

Flusso end-to-end

Il flusso completo di una push notification attraversa cinque passaggi. Primo, l'applicazione richiede il permesso all'utente tramite Notification.requestPermission() e, se concesso, invoca pushManager.subscribe() con la chiave pubblica VAPID del server. Secondo, la PushSubscription risultante viene inviata al server applicativo e persistita (tipicamente in un database). Terzo, quando il server deve inviare una notifica, cifra il payload con le chiavi della sottoscrizione e invia una richiesta HTTP POST all'endpoint, includendo il token VAPID nell'header Authorization. Quarto, il push service consegna il messaggio al browser dell'utente, che risveglia il service worker e genera un evento push. Quinto, il service worker processa il payload e invoca self.registration.showNotification() per visualizzare la notifica di sistema [17].

Un vincolo architetturale fondamentale è che il service worker deve mostrare una notifica visibile in risposta a ogni evento push. I browser impongono questa regola per prevenire l'uso silenzioso delle push notification come meccanismo di tracking: se il service worker riceve un evento push senza invocare showNotification(), il browser può visualizzare una notifica generica predefinita o revocare il permesso. Questa limitazione distingue le push notification web da quelle native, dove il sistema operativo consente aggiornamenti in background senza feedback visivo.

Cifratura e sicurezza

La cifratura del payload è obbligatoria nel protocollo Web Push e avviene con uno schema basato su Elliptic Curve Diffie-Hellman (ECDH) e HKDF (HMAC-based Key Derivation Function) [18]. Il server applicativo e il browser negoziano un segreto condiviso a partire dalle chiavi pubbliche scambiate durante la sottoscrizione, e ogni messaggio viene cifrato con AES-128-GCM utilizzando un nonce unico. Questa cifratura end-to-end garantisce che il push service, pur gestendo il trasporto, non possa leggere il contenuto dei messaggi, una proprietà di privacy fondamentale, considerando che i push service sono operati dai vendor dei browser.


Workbox: astrazione e automazione

Motivazione e architettura modulare

L'implementazione manuale delle strategie di caching, del precaching versionato e della gestione del ciclo di vita del service worker richiede una quantità significativa di codice boilerplate e una gestione attenta di edge case (risposte opache da richieste cross-origin, quota di storage, invalidazione di cache obsolete). Workbox, sviluppato dal team Chrome di Google, è una libreria open-source che astrae questi pattern in un'API dichiarativa e componibile [20, 21]. L'adozione è pervasiva: il 54% dei siti mobile che utilizzano un service worker impiega Workbox, e la libreria è integrata nelle toolchain di Angular CLI, Create React App e Vue CLI [20].

L'architettura di Workbox è modulare: ogni funzionalità è isolata in un pacchetto npm indipendente, consentendo il tree-shaking e l'inclusione selettiva delle sole funzionalità necessarie. I moduli principali sono workbox-precaching, workbox-routing, workbox-strategies, workbox-expiration, workbox-cacheable-response e workbox-background-sync [20, 21].

Precaching con revisione automatica

Il modulo workbox-precaching implementa il pattern di precaching con un meccanismo di revisione basato su hash del contenuto [21]. Durante la fase di build, un plugin (disponibile per Webpack, Rollup e altri bundler) genera un manifest di precaching, un array di oggetti {url, revision} dove revision è un hash del file. Il service worker utilizza questo manifest per determinare quali risorse devono essere scaricate (nuove o modificate) e quali possono essere mantenute dalla cache esistente. L'operazione è idempotente: eseguire il precaching con lo stesso manifest non genera richieste di rete.

La classe PrecacheRoute integra il precaching con il sistema di routing: le richieste per risorse precached vengono automaticamente intercettate e servite dalla cache, con supporto per la riscrittura degli URL (ad esempio, servire /index.html per navigazioni verso /). Questo elimina la necessità di scrivere manualmente gli handler fetch per le risorse statiche.

Routing e strategie

Il modulo workbox-routing implementa un router che associa pattern URL a strategie di caching. La registrazione avviene tramite registerRoute(match, handler), dove match può essere una stringa, un'espressione regolare o una funzione di callback, e handler è un'istanza di una delle strategie fornite da workbox-strategies [20, 21]:

import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, CacheFirst, NetworkFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

// Risorse statiche: Cache First con scadenza
registerRoute(
  ({request}) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images-cache',
    plugins: [
      new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 }),
      new CacheableResponsePlugin({ statuses: [0, 200] })
    ]
  })
);

// Risposte API: Network First con fallback
registerRoute(
  ({url}) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 3
  })
);

// Font Google: Stale While Revalidate
registerRoute(
  ({url}) => url.origin === 'https://fonts.googleapis.com',
  new StaleWhileRevalidate({ cacheName: 'google-fonts' })
);

L'architettura a plugin di Workbox consente di comporre comportamenti aggiuntivi sulle strategie base. ExpirationPlugin gestisce la rimozione automatica delle entry in base a un limite numerico o temporale, risolvendo il problema della crescita incontrollata delle cache. CacheableResponsePlugin filtra le risposte in base allo status code, prevenendo la memorizzazione di errori. BackgroundSyncPlugin integra la Background Sync API con la coda di retry automatica per richieste fallite [20].

GenerateSW e InjectManifest

Workbox offre due modalità di generazione del service worker. GenerateSW produce un service worker completo a partire da una configurazione dichiarativa, adatto ad applicazioni con esigenze di caching standard. InjectManifest inietta il manifest di precaching in un file service worker scritto manualmente, offrendo il pieno controllo sulla logica custom pur mantenendo i benefici del precaching automatizzato [21]. La scelta tra le due modalità dipende dalla complessità dei requisiti: applicazioni che necessitano di push notification, Background Sync o logica custom nell'evento fetch tipicamente adottano InjectManifest.


Limiti, compatibilità e problemi aperti

Frammentazione del supporto cross-browser

Il limite più significativo delle PWA nel 2026 è la disomogeneità del supporto tra browser e piattaforme, con le restrizioni più impattanti concentrate sull'ecosistema Apple [11]. Safari su iOS ha introdotto il supporto per le push notification solo a partire da iOS 16.4 (marzo 2023), ma con vincoli rilevanti: le notifiche funzionano esclusivamente per PWA installate sulla schermata Home, non dalle schede di Safari, e la richiesta di permesso è possibile solo in risposta a un'interazione esplicita dell'utente [11]. La Background Sync API rimane non supportata su WebKit, così come le API Web Bluetooth, Web NFC e il prompt di installazione automatico.

Una criticità specifica riguarda l'Unione Europea: con iOS 17.4, Apple ha rimosso il supporto PWA standalone nei paesi UE in risposta al Digital Markets Act, facendo sì che le PWA vengano aperte in schede Safari senza supporto push [11]. Sebbene iOS 18.2 consenta teoricamente motori di rendering di terze parti nell'UE attraverso BrowserEngineKit, l'implementazione ha introdotto barriere tali che, alla data attuale (aprile 2026), nessun browser alternativo ha adottato il framework [11]. Questa situazione crea un'asimmetria significativa nella capacità delle PWA di raggiungere gli utenti iOS europei.

Limiti del modello di storage

La Cache Storage API e IndexedDB sono soggette a quote di storage che variano per browser: Chrome consente fino al 60% dello spazio disco disponibile per origine, Firefox fino al 50% [5]. Tuttavia, in condizioni di pressione sullo storage, il browser può procedere all'eviction di entrambi i meccanismi secondo una politica Least Recently Used, senza notifica all'applicazione. Questo comportamento rende lo storage locale un meccanismo di persistenza non garantito per impostazione predefinita. L'API Storage Manager consente di richiedere la persistenza esplicita tramite navigator.storage.persist(), riducendo il rischio di eviction, ma la concessione è a discrezione del browser e dipende da fattori quali la frequenza di utilizzo e l'installazione come PWA. Per dati critici che l'utente si aspetta di ritrovare (documenti non salvati, transazioni in coda), la progettazione deve prevedere una strategia di sincronizzazione server con conferma di ricezione, trattando lo storage locale come cache temporanea e non come fonte di verità.

Complessità del versionamento

La gestione dell'aggiornamento dei service worker in presenza di versioni multiple in produzione è una fonte ricorrente di bug. Il pattern in cui un utente ha una scheda aperta con la versione N del service worker e una nuova scheda con la versione N+1 in attesa crea scenari in cui risorse della versione N vengono servite a pagine che si aspettano la versione N+1 [8]. Le soluzioni, skipWaiting combinato con clients.claim, prompt di reload all'utente, o architetture che tollerano la coesistenza di versioni, aggiungono complessità che non ha equivalente nelle applicazioni native, dove il sistema operativo gestisce atomicamente l'aggiornamento.

Risultati empirici e adozione

Nonostante i limiti descritti, i dati di adozione su larga scala documentano l'efficacia del modello PWA in produzione. Pinterest ha ridotto il bundle core da 650 KB a 150 KB, registrando un aumento del 40% nel tempo trascorso sulla piattaforma e del 60% nell'engagement core [15]. Starbucks ha raddoppiato gli utenti attivi giornalieri con una PWA il cui peso è del 99.84% inferiore all'applicazione iOS nativa [15]. Lancôme ha migliorato il punteggio Lighthouse da 23 a 94, con un incremento del 17% nelle conversioni e del 53% nelle sessioni mobile su iOS [15]. Questi risultati suggeriscono che, per applicazioni con requisiti di engagement, prestazioni di caricamento e raggiungibilità cross-platform, il modello PWA offre un rapporto beneficio/complessità favorevole, a condizione che si gestiscano esplicitamente le asimmetrie di piattaforma documentate in questa sezione.

Direzioni future

L'evoluzione delle PWA è orientata su tre direttrici. La prima è l'espansione delle capability API attraverso Project Fugu, che continua ad aggiungere accesso a funzionalità hardware (sensori, dispositivi USB, NFC) precedentemente esclusive delle applicazioni native [6]. La seconda è la maturazione delle API di storage persistente e della Periodic Background Sync API, che consentirebbe aggiornamenti periodici del contenuto in background senza interazione dell'utente, attualmente supportata solo in Chromium e ancora in fase di incubazione al W3C [22]. La terza è il miglioramento dell'interoperabilità cross-browser, con la pressione regolamentare del Digital Markets Act che potrebbe, nel medio termine, costringere Apple a garantire un supporto più completo delle API PWA su WebKit, sebbene le resistenze implementative osservate finora non incoraggino previsioni ottimistiche.


Riferimenti

[1] A. Russell, "Progressive Web Apps: Escaping Tabs Without Losing Our Soul," Infrequently Noted, 2015. https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/

[2] W3C, "Service Workers Nightly," W3C Editor's Draft, 2024. https://www.w3.org/TR/service-workers/

[3] MDN Web Docs, "Service Worker API," Mozilla Developer Network, 2024. https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API

[4] W3C, "Web Application Manifest," W3C Working Draft, 2024. https://www.w3.org/TR/appmanifest/

[5] MDN Web Docs, "Cache — Web APIs," Mozilla Developer Network, 2024. https://developer.mozilla.org/en-US/docs/Web/API/Cache

[6] Chromium Project, "Web Capabilities (Project Fugu)," 2024. https://www.chromium.org/teams/web-capabilities-fugu/

[7] A. Biørn-Hansen, T.A. Majchrzak, T.-M. Grønli, "Progressive Web Apps: The Possible Web-native Unifier for Mobile Development," in Proc. WEBIST, 2017. https://www.scitepress.org/papers/2017/63537/63537.pdf

[8] J. Archibald, "The Service Worker Lifecycle," web.dev, 2024. https://web.dev/articles/service-worker-lifecycle

[9] Google Developers, "Web App Manifest," web.dev, 2024. https://web.dev/learn/pwa/web-app-manifest

[10] Google Developers, "What Does It Take to Be Installable?," web.dev, 2024. https://web.dev/articles/install-criteria

[11] Brainhub, "PWA on iOS — Current Status & Limitations," 2025. https://brainhub.eu/library/pwa-on-ios

[12] Google Developers, "Strategies for Service Worker Caching," Chrome for Developers, 2024. https://developer.chrome.com/docs/workbox/caching-strategies-overview

[13] Google Developers, "Caching," web.dev, 2024. https://web.dev/learn/pwa/caching

[14] Google Developers, "Service Workers & Cache Storage," web.dev, 2024. https://web.dev/articles/service-workers-cache-storage

[15] PWA Stats, "A community-driven list of stats and news related to Progressive Web Apps," 2024. https://www.pwastats.com/

[16] MDN Web Docs, "Background Synchronization API," Mozilla Developer Network, 2024. https://developer.mozilla.org/en-US/docs/Web/API/Background_Synchronization_API

[17] W3C, "Push API," W3C Working Draft, 2024. https://www.w3.org/TR/push-api/

[18] M. Thomson, "Generic Event Delivery Using HTTP Push," RFC 8030, IETF, 2016. https://datatracker.ietf.org/doc/html/rfc8030

[19] M. Thomson, P. Beverloo, "Voluntary Application Server Identification (VAPID) for Web Push," RFC 8292, IETF, 2017. https://datatracker.ietf.org/doc/html/rfc8292

[20] Google Developers, "Workbox," web.dev, 2024. https://web.dev/learn/pwa/workbox

[21] Google Developers, "Workbox Modules," Chrome for Developers, 2024. https://developer.chrome.com/docs/workbox/modules

[22] WICG, "Web Periodic Background Synchronization," Web Incubator Community Group Draft, 2024. https://wicg.github.io/background-sync/spec/PeriodicBackgroundSync-index.html

Progressive Web Apps (PWA)

Raccontaci la situazione. Rispondiamo entro 24 ore nei giorni lavorativi.

Tweaks

Light mode
Atmospheric (glass)
Client logos
Terminal hero