Una shell dell'applicazione è un codice HTML, CSS e JavaScript minimo che attiva l'interfaccia utente. La shell dell'applicazione deve:
- si caricano rapidamente
- memorizzare nella cache
- visualizzare dinamicamente i contenuti
Il segreto per ottenere prestazioni affidabili in modo affidabile è una shell dell'applicazione. Pensa alla shell dell'app come al pacchetto di codice che pubblicheresti in uno store se stessi creando un'app nativa. È il carico necessario per decollare, ma potrebbe non essere la soluzione completa. Mantiene locale l'interfaccia utente e inserisce i contenuti in modo dinamico tramite un'API.
Contesto
L'articolo di Alex Russell sulle app web progressive descrive come un'app web può cambiare progressivamente attraverso l'uso e il consenso degli utenti per fornire un'esperienza più simile a un'app nativa, completa di supporto offline, notifiche push e la possibilità di essere aggiunta alla schermata Home. Dipende molto dai vantaggi in termini di funzionalità e prestazioni del service worker e delle capacità di memorizzazione nella cache. Ciò ti consente di concentrarti sulla velocità, offrendo alle tue app web lo stesso caricamento istantaneo e aggiornamenti regolari che sei abituato a vedere nelle applicazioni native.
Per sfruttare appieno queste funzionalità, abbiamo bisogno di un nuovo modo di pensare ai siti web: l'architettura shell dell'applicazione.
Scopriamo come strutturare la tua app utilizzando un'architettura shell delle applicazioni aumentata per i service worker. Esamineremo il rendering lato client e lato server e condivideremo un esempio end-to-end che puoi provare oggi stesso.
Per sottolineare questo punto, l'esempio seguente mostra il primo caricamento di un'app utilizzando questa architettura. Nota che nella parte inferiore dello schermo viene visualizzato il messaggio popup "L'app è pronta per l'utilizzo offline". Se in un secondo momento diventa disponibile un aggiornamento della shell, possiamo chiedere all'utente di eseguire l'aggiornamento per passare alla nuova versione.
Ancora una volta, cosa sono i Service worker?
Un service worker è uno script che viene eseguito in background, separatamente dalla pagina web. Risponde agli eventi, incluse le richieste di rete effettuate dalle pagine che pubblica e le notifiche push dal tuo server. Un service worker ha una durata intenzionalmente breve. Si attiva quando riceve un evento e viene eseguito solo per il tempo necessario all'elaborazione.
I Service worker dispongono inoltre di un insieme limitato di API rispetto a JavaScript in un normale contesto di navigazione. Si tratta di uno standard per i lavoratori sul web. Un Service worker non può accedere al DOM, ma può accedere a elementi come l'API Cache e può effettuare richieste di rete utilizzando l'API Fetch. L'API IndexedDB e postMessage() possono essere utilizzate anche per la persistenza dei dati e la messaggistica tra il service worker e le pagine che controlla. Gli eventi push inviati dal tuo server possono richiamare l'API Notification per aumentare il coinvolgimento degli utenti.
Un service worker può intercettare le richieste di rete effettuate da una pagina (che attiva un evento di recupero sul service worker) e restituire una risposta recuperata dalla rete, recuperata da una cache locale o persino creata in modo programmatico. Si tratta di un proxy programmabile nel browser. La parte interessante è che, indipendentemente da dove proviene la risposta, sembra che la pagina web non abbia alcun coinvolgimento dei service worker.
Per saperne di più sui service worker, leggi Introduzione ai service worker.
Vantaggi delle prestazioni
I service worker sono potenti per la memorizzazione nella cache offline, ma offrono anche vantaggi significativi in termini di prestazioni sotto forma di caricamento istantaneo per visite ripetute al tuo sito o alla tua app web. Puoi memorizzare nella cache la shell dell'applicazione in modo che funzioni offline e compili i relativi contenuti utilizzando JavaScript.
Alle visite ripetute, questo ti permette di mostrare pixel significativi sullo schermo senza la rete, anche se i tuoi contenuti alla fine provengono da lì. È come mostrare barre degli strumenti e schede immediatamente e poi caricare il resto dei contenuti in modo progressivo.
Per testare questa architettura su dispositivi reali, abbiamo eseguito l'esempio della shell dell'applicazione su WebPageTest.org e abbiamo mostrato i risultati di seguito.
Test 1: test su cavo con un Nexus 5 utilizzando Chrome Dev
La prima visualizzazione dell'app deve recuperare tutte le risorse dalla rete e non raggiunge una visualizzazione significativa prima di 1,2 secondi. Grazie alla memorizzazione nella cache del service worker, la nostra visita ripetuta raggiunge una struttura significativa e termina il caricamento in 0,5 secondi.
Test 2: Test su 3G con un Nexus 5 utilizzando Chrome Dev
Possiamo anche testare il campione con una connessione 3G leggermente più lenta. Questa volta ci vogliono 2,5 secondi alla prima visita per la nostra prima visualizzazione significativa. Il caricamento completo della pagina richiede 7,1 secondi. Con la memorizzazione nella cache del service worker, la nostra visita ripetuta raggiunge una visualizzazione significativa e termina il caricamento in 0,8 secondi.
Le altre visualizzazioni raccontano una storia simile. Confronta i 3 secondi necessari per ottenere la prima visualizzazione significativa nella shell dell'applicazione:
ai 0,9 secondi necessari quando la stessa pagina viene caricata dalla cache del service worker. I nostri utenti finali risparmiano più di 2 secondi di tempo.
Si possono ottenere risultati simili e affidabili in termini di prestazioni per le tue applicazioni utilizzando l'architettura shell dell'applicazione.
Il service worker ci richiede di ripensare il modo in cui strutturamo le app?
I service worker implicano alcune lievi modifiche all'architettura dell'applicazione. Invece di comprimere tutta la tua applicazione in una stringa HTML, può essere utile eseguire operazioni in stile AJAX. Qui è presente una shell (che viene sempre memorizzata nella cache e può avviarsi sempre senza la rete) e i contenuti vengono aggiornati regolarmente e gestiti separatamente.
Le implicazioni di questa suddivisione sono notevoli. Alla prima visita puoi eseguire il rendering dei contenuti sul server e installare il service worker sul client. Nelle visite successive è sufficiente richiedere i dati.
E il miglioramento progressivo?
Sebbene il service worker non sia attualmente supportato da tutti i browser, l'architettura shell dei contenuti dell'applicazione utilizza un miglioramento progressivo per garantire che tutti possano accedere ai contenuti. Prendiamo come esempio il nostro progetto di esempio.
Qui sotto puoi vedere la versione completa di cui è stato eseguito il rendering in Chrome, Firefox Nightly e Safari. A sinistra puoi vedere la versione di Safari, in cui i contenuti vengono visualizzati sul server senza un service worker. A destra sono presenti le versioni Chrome e Firefox Nightly basate sul service worker.
Quando è consigliabile utilizzare questa architettura?
L'architettura shell dell'applicazione è più adatta alle app e ai siti dinamici. Se il tuo sito è di piccole dimensioni e statico, probabilmente non hai bisogno di una shell dell'applicazione e puoi semplicemente memorizzare nella cache l'intero sito in un passaggio oninstall
del service worker. Utilizza l'approccio più adatto al tuo progetto. Alcuni framework JavaScript incoraggiano già la suddivisione della logica dell'applicazione dai contenuti, rendendo questo modello più semplice da applicare.
Esistono ancora app di produzione che utilizzano questo pattern?
L'architettura shell dell'applicazione è possibile con poche modifiche all'interfaccia utente complessiva dell'applicazione e ha funzionato bene per siti su larga scala come l'app web progressiva I/O 2015 di Google e la posta in arrivo di Google.
Le shell delle applicazioni offline sono un importante vantaggio in termini di prestazioni e sono risultate efficaci anche nell'app Wikipedia offline di Jake Archibald e nell'app web progressiva di Flipkart Lite.
Spiegazione dell'architettura
Durante l'esperienza di primo caricamento, l'obiettivo è far visualizzare contenuti significativi sullo schermo dell'utente il più rapidamente possibile.
Primo caricamento e caricamento di altre pagine
In generale, l'architettura shell dell'applicazione:
Dai la priorità al caricamento iniziale, ma lascia che il service worker memorizzi nella cache la shell dell'applicazione in modo che le visite ripetute non richiedano che la shell venga ricaricata dalla rete.
Il caricamento lento o il caricamento in background di tutto il resto. Una buona soluzione è utilizzare la memorizzazione nella cache di lettura per i contenuti dinamici.
Utilizza strumenti dei service worker come sw-precache, ad esempio per memorizzare nella cache e aggiornare in modo affidabile il service worker che gestisce i contenuti statici. (ulteriori informazioni su sw-precache più avanti).
Per raggiungere questo obiettivo:
Il server invierà contenuti HTML che il client può visualizzare e utilizzerà intestazioni di scadenza della cache HTTP future per tenere conto dei browser senza supporto dei service worker. I nomi file verranno pubblicati tramite hash per attivare sia il controllo delle versioni sia semplici aggiornamenti per un secondo momento nel ciclo di vita dell'applicazione.
Le pagine includeranno stili CSS incorporati in un tag
<style>
all'interno del documento<head>
per fornire una prima visualizzazione rapida della shell dell'applicazione. Ogni pagina caricherà in modo asincrono il codice JavaScript necessario per la visualizzazione corrente. Poiché il codice CSS non può essere caricato in modo asincrono, possiamo richiedere gli stili utilizzando JavaScript perché è asincrono anziché basato sull'analizzatore sintattico e sincrono. Possiamo anche sfruttarerequestAnimationFrame()
per evitare i casi in cui potremmo ricevere un rapido successo della cache e finire per ritrovare stili che entrano accidentalmente a far parte del percorso di rendering critico.requestAnimationFrame()
forza la colorazione del primo frame prima del caricamento degli stili. Un'altra opzione è quella di utilizzare progetti come loadCSS di Filament Group per richiedere CSS in modo asincrono utilizzando JavaScript.Il Service worker memorizzerà una voce memorizzata nella cache della shell dell'applicazione in modo che, in occasione di visite ripetute, la shell possa essere caricata interamente dalla cache del service worker, a meno che non sia disponibile un aggiornamento sulla rete.
Un'implementazione pratica
Abbiamo scritto un esempio completamente funzionante utilizzando l'architettura della shell dell'applicazione, JavaScript Vanilla ES2015 per il client ed Express.js per il server. Ovviamente nulla ti impedisce di utilizzare il tuo stack per il client o per le parti del server (ad es.PHP, Ruby, Python).
Ciclo di vita del Service worker
Per il progetto shell dell'applicazione utilizziamo sw-precache, che offre il seguente ciclo di vita del service worker:
Evento | Azione |
---|---|
Installa | Memorizzare nella cache la shell dell'applicazione e altre risorse dell'app a pagina singola. |
Attivazione | Svuota le vecchie cache. |
Recupero | Pubblica un'app web su una sola pagina per gli URL e utilizza la cache per gli asset e le parziali predefinite. Usa la rete per altre richieste. |
Bit del server
In questa architettura, un componente lato server (nel nostro caso, scritto in formato Express) dovrebbe essere in grado di trattare i contenuti e la presentazione separatamente. I contenuti possono essere aggiunti a un layout HTML che genera un rendering statico della pagina oppure essere pubblicati separatamente e caricati dinamicamente.
Comprensibilmente, la tua configurazione lato server può essere notevolmente diversa da quella che utilizziamo per la nostra app demo. Questo pattern di app web è raggiungibile dalla maggior parte delle configurazioni dei server, anche se richiede una certa riprogettazione. Abbiamo riscontrato che il seguente modello funziona abbastanza bene:
Gli endpoint sono definiti per tre parti dell'applicazione: l'URL rivolto all'utente (index/wildcard), la shell dell'applicazione (service worker) e le parziali HTML.
Ogni endpoint ha un controller che inserisce un layout di manubri che a sua volta può inserire immagini e parziali del manubrio. In poche parole, le parziali sono visualizzazioni che sono blocchi di HTML copiati nella pagina finale. Nota: i framework JavaScript che eseguono una sincronizzazione dei dati più avanzata sono spesso molto più facili da trasferire a un'architettura di Application Shell. Tendono a utilizzare le associazioni e la sincronizzazione dei dati anziché le parziali.
All'utente viene inizialmente mostrata una pagina statica con contenuti. In questa pagina viene registrato un service worker, se supportato, che memorizza nella cache la shell dell'applicazione e tutto ciò da cui dipende (CSS, JS e così via).
La shell dell'app agirà quindi come un'app web su una singola pagina, utilizzando JavaScript in XHR nei contenuti di un URL specifico. Le chiamate XHR vengono effettuate a un endpoint /partials* che restituisce il frammento di codice HTML, CSS e JS necessario per visualizzare quel contenuto. Nota: esistono molti modi per affrontare questo problema e l'XHR è solo uno di questi. Alcune applicazioni incorporeranno i propri dati (magari utilizzando JSON) per il rendering iniziale e pertanto non sono "statici" nel senso dell'HTML bidimensionale.
Ai browser senza supporto dei service worker dovrebbe essere sempre offerta un'esperienza di riserva. Nella nostra demo, ricorriamo al rendering statico lato server di base, ma questa è solo una delle tante opzioni. L'aspetto del service worker ti offre nuove opportunità per migliorare le prestazioni della tua app in stile applicazione a pagina singola utilizzando la shell dell'applicazione memorizzata nella cache.
Controllo delle versioni dei file
Una domanda che si chiede è come gestire il controllo delle versioni e l'aggiornamento dei file. Questo è specifico per l'applicazione e le opzioni sono:
prima sulla rete, altrimenti utilizza la versione memorizzata nella cache.
Solo rete e errore se offline.
Memorizza nella cache la versione precedente ed esegui l'aggiornamento in un secondo momento.
Per la shell dell'applicazione stessa, è necessario adottare un approccio incentrato sulla cache per la configurazione del service worker. Se non stai memorizzando nella cache la shell dell'applicazione, non hai adottato correttamente l'architettura.
Utensili
Gestiamo diverse librerie helper per i service worker che semplificano la configurazione del processo di pre-memorizzazione nella cache della shell dell'applicazione o di gestione di pattern di memorizzazione nella cache comuni.
Utilizza sw-precache per la shell dell'applicazione
L'uso di sw-precache per memorizzare nella cache la shell dell'applicazione dovrebbe consentirti di gestire i problemi relativi alle revisioni dei file, alle domande di installazione/attivazione e allo scenario di recupero per la shell dell'app. Inserisci sw-precache nel processo di compilazione della tua applicazione e utilizza caratteri jolly configurabili per raccogliere le risorse statiche. Anziché creare manualmente lo script del service worker, consenti a sw-precache di generarne uno che gestisca la cache in modo sicuro ed efficiente, utilizzando un gestore di recupero "cache-first".
Le visite iniziali alla tua app attivano la pre-memorizzazione nella cache dell'insieme completo di risorse necessarie. Si tratta di un'esperienza simile all'installazione di un'app nativa da uno store. Quando gli utenti tornano alla tua app, vengono scaricate solo le risorse aggiornate. Nella nostra demo, informiamo gli utenti quando è disponibile una nuova shell con il messaggio "Aggiornamenti dell'app. Aggiorna per la nuova versione." Questo pattern è un modo semplice per informare gli utenti che possono eseguire l'aggiornamento alla versione più recente.
Usa sw-toolbox per la memorizzazione nella cache di runtime
Utilizza sw-toolbox per la memorizzazione nella cache di runtime con strategie diverse a seconda della risorsa:
cacheFirst per le immagini, oltre a una cache denominata dedicata con un criterio di scadenza personalizzato di N maxEntries.
networkFirst o più veloce per le richieste API, a seconda dell'aggiornamento dei contenuti desiderato. Il metodo più veloce potrebbe andare bene, ma se esiste un feed API specifico che viene aggiornato di frequente, utilizza networkFirst.
Conclusione
Le architetture shell dell'applicazione offrono diversi vantaggi, ma hanno senso solo per alcune classi di applicazioni. Il modello è ancora giovane e varrà la pena valutare l'impegno e i vantaggi complessivi in termini di prestazioni di questa architettura.
Nei nostri esperimenti, abbiamo sfruttato la condivisione dei modelli tra il client e il server per ridurre al minimo il lavoro di creazione di due livelli di applicazione. Ciò garantisce che il miglioramento progressivo sia ancora una funzionalità fondamentale.
Se stai già prendendo in considerazione l'utilizzo dei service worker nella tua app, dai un'occhiata all'architettura e valuta se è adatta ai tuoi progetti.
Grazie ai nostri revisori: Jeff Posnick, Paul Lewis, Alex Russell, Seth Thompson, Rob Dodson, Taylor Savage e Joe Medley.