Caricamento istantaneo delle app web con un'architettura della shell dell'applicazione

Una shell dell'applicazione è il codice HTML, CSS e JavaScript minimo che supporta un'interfaccia utente. La shell dell'applicazione deve:

  • caricamento rapido
  • essere memorizzati nella cache
  • visualizzare i contenuti in modo dinamico

Una shell dell'applicazione è il segreto per prestazioni affidabili. Pensa alla shell della tua app come al bundle di codice che pubblicheresti in un app store se stessi creando un'app nativa. È il carico necessario per iniziare, ma potrebbe non essere tutto. Mantiene l'interfaccia utente locale e recupera i contenuti in modo dinamico tramite un'API.

Separazione della shell dell'app da HTML, JS e CSS e dai contenuti HTML

Sfondo

L'articolo di Alex Russell sulle app web progressive descrive in che modo un'app web può cambiare progressivamente con l'uso e il consenso dell'utente per offrire un'esperienza più simile a quella di 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 dei service worker e dalle loro capacità di memorizzazione nella cache. In questo modo puoi concentrarti sulla velocità, offrendo alle tue app web lo stesso caricamento istantaneo e gli aggiornamenti regolari che vedi nelle applicazioni native.

Per sfruttare appieno queste funzionalità, abbiamo bisogno di un nuovo modo di pensare ai siti web: l'architettura dell'involucro dell'applicazione.

Scopri come strutturare la tua app utilizzando un'architettura di shell dell'applicazione con service worker. Esamineremo il rendering sia lato client sia lato server e condivideremo un esempio end-to-end che puoi provare oggi stesso.

Per sottolineare il punto, l'esempio seguente mostra il primo caricamento di un'app che utilizza questa architettura. Nota il messaggio popup "L'app è pronta per l'utilizzo offline" nella parte inferiore dello schermo. Se in un secondo momento diventa disponibile un aggiornamento della shell, possiamo informare l'utente di eseguire l'aggiornamento alla nuova versione.

Immagine del worker di servizio in esecuzione in DevTools per il shell dell'applicazione

Che cosa sono i service worker?

Un service worker è uno script che viene eseguito in background, separatamente dalla pagina web. Risponde agli eventi, tra cui le richieste di rete effettuate dalle pagine che pubblica e le notifiche push dal tuo server. La durata di un worker di servizio è intenzionalmente breve. Si attiva quando riceve un evento ed esegue solo il tempo necessario per elaborarlo.

I worker di servizio hanno anche un insieme limitato di API rispetto a JavaScript in un normale contesto di navigazione. Si tratta di una pratica standard per i worker sul web. Un worker di servizio non può accedere al DOM, ma può accedere ad elementi come l'API Cache ed eseguire richieste di rete utilizzando l'API Fetch. L'API IndexedDB e postMessage() sono disponibili anche per la persistenza dei dati e la messaggistica tra il worker di servizio 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 attivano un evento di recupero sul service worker) e restituire una risposta recuperata dalla rete, da una cache locale o persino costruita tramite programmazione. In pratica, si tratta di un proxy programmabile nel browser. La parte interessante è che, indipendentemente da dove proviene la risposta, per la pagina web è come se il service worker non fosse stato coinvolto.

Per scoprire di più sui service worker, leggi l'introduzione ai service worker.

Vantaggi in termini di rendimento

I worker di servizio sono molto efficaci per la memorizzazione nella cache offline, ma offrono anche significativi miglioramenti delle prestazioni sotto forma di caricamento istantaneo per le visite ripetute al tuo sito o alla tua app web. Puoi memorizzare nella cache la shell dell'applicazione in modo che funzioni offline e compilarne i contenuti utilizzando JavaScript.

In caso di visite ripetute, puoi ottenere pixel significativi sullo schermo senza la rete, anche se i tuoi contenuti provengono da lì. Puoi visualizzare le barre degli strumenti e le schede immediatamente, quindi caricare il resto dei contenuti progressivamente.

Per testare questa architettura su dispositivi reali, abbiamo eseguito il nostro esempio di shell dell'applicazione su WebPageTest.org e mostrato i risultati di seguito.

Test 1: test con cavo su Nexus 5 utilizzando Chrome Dev

La prima visualizzazione dell'app deve recuperare tutte le risorse dalla rete e non raggiunge un rendering significativo fino a 1,2 secondi. Grazie alla memorizzazione nella cache dei worker di servizio, la nostra visita ripetuta ottiene una visualizzazione significativa e completa il caricamento in 0,5 secondi.

Diagramma di pittura del test della pagina web per il collegamento del cavo

Test 2: test su rete 3G con un Nexus 5 utilizzando Chrome Dev

Possiamo anche testare il nostro sample con una connessione 3G leggermente più lenta. Questa volta sono necessari 2,5 secondi alla prima visita per la prima visualizzazione con contenuti. Il caricamento completo della pagina richiede 7,1 secondi. Con la memorizzazione nella cache dei worker di servizio, la nostra visita ripetuta ottiene una visualizzazione significativa e completa il caricamento in 0,8 secondi.

Diagramma di pittura del test della pagina web per la connessione 3G

Altre visualizzazioni raccontano una storia simile. Confronta i 3 secondi necessari per ottenere la prima visualizzazione con contenuti nella shell dell'applicazione:

Tempistica di Paint per la prima visualizzazione dal test della pagina web

rispetto ai 0,9 secondi necessari per caricare la stessa pagina dalla cache del nostro service worker. I nostri utenti finali risparmiano più di 2 secondi.

Visualizzare la sequenza temporale della visualizzazione ripetuta dal test della pagina web

È possibile ottenere prestazioni simili e affidabili per le tue applicazioni utilizzando l'architettura dell'involucro dell'applicazione.

I worker di servizio ci richiedono di ripensare alla struttura delle app?

I worker di servizio implicano alcune modifiche sottili nell'architettura dell'applicazione. Anziché comprimere tutta l'applicazione in una stringa HTML, può essere utile eseguire operazioni in stile AJAX. Qui hai una shell (che viene sempre memorizzata nella cache e può sempre avviarsi senza la rete) e contenuti che 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 dovrai solo richiedere i dati.

Che ne dici del potenziamento progressivo?

Sebbene i worker di servizio non siano attualmente supportati da tutti i browser, l'architettura della shell dei contenuti dell'applicazione utilizza il miglioramento progressivo per garantire che tutti possano accedere ai contenuti. Ad esempio, prendiamo il nostro progetto di esempio.

Di seguito puoi vedere la versione completa visualizzata in Chrome, Firefox Nightly e Safari. All'estrema sinistra puoi vedere la versione di Safari in cui i contenuti vengono visualizzati sul server senza un worker di servizio. A destra vediamo le versioni Nightly di Chrome e Firefox basate su service worker.

Immagine di Application Shell caricata in Safari, Chrome e Firefox

Quando è opportuno utilizzare questa architettura?

L'architettura della shell dell'applicazione è più adatta per app e 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. Diversi framework JavaScript incoraggiano già a separare la logica dell'applicazione dai contenuti, rendendo più semplice l'applicazione di questo pattern.

Esistono già app di produzione che utilizzano questo pattern?

L'architettura della shell dell'applicazione è possibile con poche modifiche all'interfaccia utente complessiva dell'applicazione e ha funzionato bene per siti di grandi dimensioni come la Progressive Web App I/O 2015 di Google e la Posta in arrivo di Google.

Immagine del caricamento della Posta in arrivo di Google. Illustra la Posta in arrivo che utilizza il service worker.

Le shell delle applicazioni offline sono un vantaggio importante per le prestazioni e sono ben dimostrate anche nell'app Wikipedia offline di Jake Archibald e nell'app web progressiva di Flipkart Lite.

Screenshot della demo di Wikipedia di Jake Archibald.

Spiegazione dell'architettura

Durante la prima esperienza di caricamento, il tuo obiettivo è mostrare contenuti significativi sullo schermo dell'utente il più rapidamente possibile.

Primo caricamento e caricamento di altre pagine

Diagramma del primo caricamento con la shell dell'app

In generale, l'architettura della 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 il recupero della shell dalla rete.

  • Carica in modo lazy o in background tutto il resto. Una buona opzione è utilizzare la cache di lettura per i contenuti dinamici.

  • Utilizza gli strumenti per i worker di servizio, come sw-precache, ad esempio per memorizzare nella cache e aggiornare in modo affidabile il worker di servizio che gestisce i contenuti statici. (Scopri di più su sw-precache più avanti).

Per farlo:

  • Il server invierà contenuti HTML che il client può visualizzare e utilizzerà intestazioni di scadenza della cache HTTP molto distanti nel tempo per tenere conto dei browser senza supporto di service worker. Servirà i nomi file utilizzando gli hash per abilitare sia il "controllo delle versioni" sia gli aggiornamenti facili in un secondo momento nel ciclo di vita dell'applicazione.

  • Pagine includerà gli stili CSS in linea 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 CSS non può essere caricato in modo asincrono, possiamo richiedere gli stili utilizzando JavaScript perché è asincrono anziché basato su parser e sincrono. Possiamo anche sfruttare requestAnimationFrame() per evitare i casi in cui potremmo ottenere un rapido hit della cache e finire con gli stili che diventano accidentalmente parte del percorso di rendering critico. requestAnimationFrame() forza la visualizzazione del primo frame prima del caricamento degli stili. Un'altra opzione è utilizzare progetti come loadCSS di Filament Group per richiedere CSS in modo asincrono utilizzando JavaScript.

  • Il service worker memorizza una voce memorizzata nella cache della shell dell'applicazione in modo che, in caso di visite ripetute, la shell possa essere caricata interamente dalla cache del service worker, a meno che non sia disponibile un aggiornamento sulla rete.

Shell dell&#39;app per i contenuti

Un'implementazione pratica

Abbiamo scritto un esempio completamente funzionante utilizzando l'architettura dell'involucro dell'applicazione, JavaScript ES2015 standard per il client ed Express.js per il server. Ovviamente, nulla ti impedisce di utilizzare il tuo stack per le parti client o server (ad es.PHP, Ruby, Python).

Ciclo di vita del service worker

Per il nostro progetto shell dell'applicazione, utilizziamo sw-precache, che offre il seguente ciclo di vita del servizio worker:

Evento Azione
Installa Memorizza nella cache la shell dell'applicazione e altre risorse delle app a pagina singola.
Attiva Svuota le cache vecchie.
Recupero Pubblica un'app web a pagina singola per gli URL e utilizza la cache per gli asset e i componenti parziali predefiniti. Utilizza la rete per altre richieste.

Bit del server

In questa architettura, un componente lato server (nel nostro caso, scritto in 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 possono essere pubblicati separatamente e caricati in modo dinamico.

È comprensibile che la configurazione lato server possa essere molto diversa da quella che utilizziamo per la nostra app di dimostrazione. Questo modello di app web è realizzabile con la maggior parte delle configurazioni del server, anche se richiede una certa ridefinizione dell'architettura. Abbiamo riscontrato che il seguente modello funziona abbastanza bene:

Diagramma dell&#39;architettura della shell dell&#39;app
  • Gli endpoint sono definiti per tre parti dell'applicazione: gli URL rivolti agli utenti (index/wildcard), la shell dell'applicazione (service worker) e i componenti HTML parziali.

  • Ogni endpoint ha un controller che recupera un layout di componenti in primo piano che a sua volta può recuperare componenti in primo piano parziali e visualizzazioni. In parole povere, i componenti parziali sono visualizzazioni, ovvero blocchi di codice HTML che vengono copiati nella pagina finale. Nota: i framework JavaScript che eseguono la sincronizzazione dei dati in modo più avanzato sono spesso molto più facili da eseguire il porting in un'architettura Application Shell. Tendono a utilizzare il binding dei dati e la sincronizzazione anziché i componenti parziali.

  • All'utente viene inizialmente mostrata una pagina statica con contenuti. Questa pagina registra un service worker, se supportato, che memorizza nella cache la shell dell'applicazione e tutto ciò di cui dipende (CSS, JS e così via).

  • La shell dell'app fungerà quindi da app web a pagina singola, utilizzando JavaScript per XHR nei contenuti per un URL specifico. Le chiamate XHR vengono effettuate a un endpoint /partials* che restituisce il piccolo frammento di HTML, CSS e JS necessario per visualizzare i contenuti. Nota: esistono molti modi per farlo e XHR è solo uno di questi. Alcune applicazioni inseriscono in linea i propri dati (magari utilizzando JSON) per il rendering iniziale e quindi non sono "statiche" nel senso di HTML appiattito.

  • Per i browser senza il supporto di Service Worker deve sempre essere pubblicata un'esperienza di riserva. Nella nostra demo, torniamo al rendering lato server statico di base, ma questa è solo una delle tante opzioni. L'aspetto del servizio worker offre nuove opportunità per migliorare il rendimento 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 sorge spontanea è come gestire il controllo delle versioni e l'aggiornamento dei file. Si tratta di un'opzione specifica per l'applicazione e le opzioni sono:

  • In primo luogo la rete e, in caso contrario, la versione memorizzata nella cache.

  • Solo rete e non funziona se offline.

  • Memorizza nella cache la versione precedente e aggiornala in un secondo momento.

Per la shell dell'applicazione stessa, devi adottare un approccio cache-first per la configurazione del tuo worker di servizio. Se non memorizzi nella cache la shell dell'applicazione, non hai adottato correttamente l'architettura.

Utensili

Gestiamo una serie di diverse librerie di assistenza per i worker di servizio che semplificano la configurazione della precache della shell dell'applicazione o la gestione di pattern di memorizzazione nella cache comuni.

Screenshot del sito della libreria Service Worker su Web Fundamentals

Utilizzare sw-precache per la shell dell'applicazione

L'utilizzo di sw-precache per memorizzare nella cache la shell dell'applicazione dovrebbe risolvere i problemi relativi alle revisioni dei file, alle domande di installazione/attivazione e allo scenario di recupero della shell dell'app. Inserisci sw-precache nel processo di compilazione dell'applicazione e utilizza caratteri jolly configurabili per recuperare le risorse statiche. Anziché creare manualmente lo script del tuo worker di servizio, lascia che sia sw-precache a 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 precaricazione dell'intero insieme di risorse necessarie. È un'esperienza simile a quella di installazione di un'app nativa da uno store. Quando gli utenti tornano nella tua app, vengono scaricate solo le risorse aggiornate. Nella nostra demo, informiamo gli utenti quando è disponibile una nuova shell con il messaggio "Aggiornamenti app. Aggiorna per la nuova versione." Questo pattern è un modo semplice per comunicare agli utenti che possono eseguire l'aggiornamento all'ultima versione.

Utilizzare 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, insieme a una cache denominata dedicata con un criterio di scadenza personalizzato di N maxEntries.

  • networkFirst o più veloce per le richieste API, a seconda della novità dei contenuti desiderata. Più veloce potrebbe andare bene, ma se esiste un feed API specifico che viene aggiornato di frequente, utilizza networkFirst.

Conclusione

Le architetture di shell delle applicazioni offrono diversi vantaggi, ma hanno senso solo per alcune classi di applicazioni. Il modello è ancora agli inizi e vale la pena valutare lo sforzo e i vantaggi generali del rendimento 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. In questo modo, il miglioramento progressivo rimane una funzionalità di base.

Se stai già valutando la possibilità di utilizzare i service worker nella tua app, dai un'occhiata all'architettura e valuta se ha senso per i tuoi progetti.

Un ringraziamento ai nostri revisori: Jeff Posnick, Paul Lewis, Alex Russell, Seth Thompson, Rob Dodson, Taylor Savage e Joe Medley.