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

Addy Osmani
Addy Osmani
Matt Gaunt

Una shell dell'applicazione è una combinazione minima di HTML, CSS e JavaScript che attiva un'interfaccia utente. La shell dell'applicazione deve:

  • caricamento veloce
  • essere memorizzati nella cache
  • visualizzare dinamicamente i contenuti

La shell dell'applicazione è il segreto per garantire prestazioni ottimali in modo affidabile. Pensa alla shell dell'app come al bundle di codice che pubblicheresti su un app store se stessi creando un'app nativa. Si tratta del carico necessario per decollare, ma potrebbe non essere l'intera storia. Mantiene la tua UI locale e inserisce i contenuti in modo dinamico tramite un'API.

Separazione della shell dell'app tra HTML, JS e CSS della shell e i contenuti HTML

Contesto

L'articolo di Alex Russell sulle app web progressive descrive come un'app web può cambiare progressivamente attraverso l'uso e il consenso dell'utente per fornire un'esperienza più simile a un'app nativa, completa di supporto offline, notifiche push e possibilità di essere aggiunta alla schermata Home. Dipende molto dai vantaggi in termini di funzionalità e prestazioni del service worker e dalle sue 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 sei abituato a vedere nelle applicazioni native.

Per sfruttare appieno queste funzionalità, abbiamo bisogno di un nuovo modo di concepire i siti web: l'architettura della shell dell'applicazione.

Diamo un'occhiata a come strutturare la tua app utilizzando un'architettura della shell dell'applicazione aumentata dei service worker. Esamineremo il rendering sia lato client che lato server e condivideremo un esempio end-to-end che puoi provare oggi stesso.

Per sottolineare questo aspetto, l'esempio seguente mostra il primo caricamento di un'app che utilizza questa architettura. Osserva il messaggio toast "L'app è pronta per l'uso offline" nella parte inferiore dello schermo. Se un aggiornamento della shell diventa disponibile in un secondo momento, possiamo comunicare all'utente di eseguire l'aggiornamento per la nuova versione.

Immagine del service worker in esecuzione in DevTools per la shell dell'applicazione

Cosa sono i service worker?

Un service worker è uno script che viene eseguito in background, separato 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 funziona solo per il tempo necessario a elaborarlo.

I service worker hanno anche un insieme limitato di API rispetto a JavaScript in un normale contesto di navigazione. Si tratta di una procedura 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 anche essere utilizzati 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 o recuperata da una cache locale o persino creata in modo programmatico. Si tratta di un proxy programmabile nel browser. La parte interessante è che, indipendentemente dall'origine della risposta, la pagina web sembra che non ci sia stato alcun coinvolgimento del service worker.

Per ulteriori informazioni sui Service worker, consulta l'articolo 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 popolarne i contenuti utilizzando JavaScript.

In caso di visite ripetute, questo ti consente di visualizzare pixel significativi sullo schermo senza rete, anche se i tuoi contenuti provengono da lì. È come visualizzare 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 il nostro esempio di shell dell'applicazione su WebPageTest.org, mostrando i risultati di seguito.

Test 1: Test sul cavo con un Nexus 5 usando Chrome Dev

La prima visualizzazione dell'app deve recuperare tutte le risorse dalla rete e raggiunge una visualizzazione significativa solo dopo 1,2 secondi. Grazie alla memorizzazione nella cache del service worker, la nostra visita ripetuta raggiunge un rendering significativo e termina il caricamento in 0,5 secondi.

Diagramma di visualizzazione del test della pagina web per la connessione del cavo

Test 2: Test su 3G con un Nexus 5 usando Chrome Dev

Possiamo anche testare il nostro campione con una connessione 3G leggermente più lenta. Questa volta, per la prima visualizzazione significativa, sono necessari 2,5 secondi alla prima visita. Il caricamento completo della pagina richiede 7,1 secondi. Con la memorizzazione nella cache del service worker, la nostra visita ripetuta raggiunge un rendering significativo e termina il caricamento completo in 0,8 secondi.

Diagramma di visualizzazione del test delle pagine web per la connessione 3G

Le altre visualizzazioni raccontano una storia simile. Confronta i 3 secondi necessari per ottenere la prima visualizzazione significativa nella shell dell'applicazione:

Visualizza la sequenza temporale per la prima visualizzazione dal test della pagina web

ai 0,9 secondi necessari quando la stessa pagina viene caricata dalla cache del nostro service worker. Per i nostri utenti finali vengono risparmiati più di 2 secondi di tempo.

Copia la sequenza temporale per la visualizzazione ripetuta dal test della pagina web

Utilizzando l'architettura della shell dell'applicazione, puoi ottenere vittorie in prestazioni simili e affidabili per le tue applicazioni.

Il service worker ci obbliga a ripensare a come strutturare le app?

I service worker implicano alcune lievi modifiche nell'architettura dell'applicazione. Anziché comprimere tutta l'applicazione in una stringa HTML, può essere utile utilizzare lo stile AJAX. È qui che hai una shell (sempre memorizzata nella cache e può sempre essere avviata senza rete) e contenuti che vengono aggiornati regolarmente e gestiti separatamente.

Le implicazioni di questa suddivisione sono ampie. Alla prima visita puoi eseguire il rendering dei contenuti sul server e installare il service worker sul client. Nelle visite successive dovrai richiedere solo i dati.

E per quanto riguarda il miglioramento progressivo?

Sebbene il service worker non sia attualmente supportato da tutti i browser, l'architettura della shell dei contenuti delle applicazioni utilizza un miglioramento progressivo per garantire che tutti possano accedere ai contenuti. Prendiamo, ad esempio, il nostro progetto di esempio.

Di seguito puoi vedere la versione completa visualizzata 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 visibili le versioni Chrome e Firefox Nightly basate sul service worker.

Immagine della shell dell'applicazione caricata in Safari, Chrome e Firefox

Quando ha senso utilizzare questa architettura?

L'architettura della shell dell'applicazione è più adatta alle app e ai siti dinamici. Se il tuo sito è piccolo e statico, probabilmente non hai bisogno di una shell dell'applicazione e puoi semplicemente memorizzare l'intero sito nella cache in un passaggio oninstall del service worker. Utilizza l'approccio più adatto al tuo progetto. Alcuni framework JavaScript incoraggiano già a suddividere la logica dell'applicazione dal contenuto, rendendo questo pattern più semplice da applicare.

Esistono ancora 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 su larga scala come l'app web progressiva I/O 2015 e la posta in arrivo di Google.

Immagine del caricamento della Posta in arrivo di Google. Illustrazione della Posta in arrivo utilizzando il service worker.

Le shell delle applicazioni offline rappresentano un'ottima vittoria in termini di prestazioni e sono state dimostrate efficaci anche nell'app Wikipedia offline di Jake Archibald e nell'app web progressiva di Flipkart Lite.

Screenshot della demo su Wikipedia di Jake Archibald.

Spiegazione dell'architettura

Durante il primo caricamento, l'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 consenti al service worker di memorizzare nella cache la shell dell'applicazione in modo che le visite ripetute non richiedano il recupero della shell dalla rete.

  • Caricamento lento o caricamento in background di tutto il resto. Una buona opzione è utilizzare la memorizzazione nella cache di lettura per i contenuti dinamici.

  • Utilizza gli 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 in seguito).

Per ottenere questo risultato:

  • Il server invia contenuti HTML che il client può visualizzare e utilizza intestazioni di scadenza della cache HTTP di lunga data per tenere conto dei browser che non supportano i service worker. Pubblicherà i nomi file utilizzando hash per abilitare sia il "controllo delle versioni" sia gli aggiornamenti semplici per le fasi successive del ciclo di vita dell'applicazione.

  • Le pagine includeranno stili CSS incorporati in un tag <style> all'interno del documento <head> per fornire una prima rappresentazione 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 in quanto è asincrono anziché sincrono e basato sull'analizzatore sintattico. Possiamo anche utilizzare requestAnimationFrame() per evitare i casi in cui potremmo ottenere un rapido successo della cache e finire con gli stili che entrano accidentalmente a far parte del percorso di rendering critico. requestAnimationFrame() impone il caricamento del primo frame prima del caricamento degli stili. Un'altra opzione consiste nell'utilizzare progetti come loadCSS di Filament Group per richiedere il codice CSS in modo asincrono utilizzando JavaScript.

  • Il Service worker memorizzerà una voce della shell dell'applicazione memorizzata nella cache 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.

Shell dell&#39;app per i contenuti

Un'implementazione pratica

Abbiamo scritto un esempio completamente funzionante utilizzando l'architettura della shell dell'applicazione, il codice JavaScript vanilla ES2015 per il client ed Express.js per il server. Naturalmente non c'è nulla che ti impedisca di utilizzare il tuo stack per le parti del client o del server (ad esempio, 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 dei service worker:

Evento Azione
Installa Memorizza nella cache la shell dell'applicazione e altre risorse dell'app a pagina singola.
Attiva Svuota le vecchie cache.
Recupero Pubblica un'app web a pagina singola 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 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 dinamicamente.

È comprensibile che la tua configurazione lato server possa essere drasticamente diversa da quella che utilizziamo per la nostra app demo. Questo modello di app web è realizzabile dalla maggior parte delle configurazioni dei server, anche se richiede una riprogettazione 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: l'URL rivolto all'utente (indice/carattere jolly), la shell dell'applicazione (service worker) e le parti HTML parziali.

  • Ogni endpoint è dotato di un controller che inserisce un layout del manubrio che a sua volta può visualizzare i dati parziali e le visualizzazioni del manubrio. In breve, le visualizzazioni parziali sono porzioni di codice HTML che vengono copiate nella pagina finale. Nota: i framework JavaScript che eseguono una sincronizzazione più avanzata dei dati sono spesso molto più semplici da trasferire in un'architettura di Application Shell. Tendono a utilizzare l'associazione di dati e la sincronizzazione piuttosto che valori parziali.

  • Inizialmente, all'utente viene 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ò da cui dipende (CSS, JS e così via).

  • La shell dell'app agirà quindi come un'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 la piccola porzione di HTML, CSS e JS necessaria per visualizzare i contenuti. Nota: ci sono molti modi per affrontare questo problema e l'XHR è solo uno di questi. Alcune applicazioni incorporeranno i propri dati (ad esempio utilizzando JSON) per il rendering iniziale e, pertanto, non saranno "statici" nel senso dell'HTML diviso.

  • Per i browser senza supporto per i service worker deve sempre essere offerta un'esperienza di riserva. Nella nostra demo, ricorriamo al rendering statico lato server di base, ma questa è solo una delle numerose 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 sorge è come gestire il controllo delle versioni e l'aggiornamento dei file. Questa opzione è specifica per l'applicazione e le opzioni sono:

  • Rete e, in caso contrario, utilizza la versione memorizzata nella cache.

  • Solo rete e non va a buon fine se offline.

  • Memorizza nella cache la versione precedente e aggiornala in seguito.

Per la shell dell'applicazione, è 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 una serie di diverse librerie helper dei service worker che semplificano il processo di precarico della shell dell'applicazione o di gestione dei pattern di memorizzazione nella cache comuni da configurare.

Screenshot del sito della libreria di Service Worker su Web Fundamentals

Utilizzo di sw-precache per la shell dell'applicazione

L'uso di sw-precache per memorizzare nella cache la shell dell'applicazione dovrebbe gestire i problemi relativi alle revisioni dei file, alle domande di installazione/attivazione e allo scenario di recupero per la shell dell'app. Trascina sw-precache nel processo di compilazione della tua applicazione e utilizza caratteri jolly configurabili per recuperare 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 delle 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 comunicare agli utenti che possono eseguire un aggiornamento all'ultima versione.

Utilizzare sw-toolkit 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 che ha un criterio di scadenza personalizzato di N maxEntries.

  • networkFirst o il più veloce per le richieste API, a seconda dell'aggiornamento dei contenuti desiderato. La velocità più veloce potrebbe andare bene, ma se c'è un feed API specifico che viene aggiornato di frequente, utilizza networkFirst.

Conclusione

Le architetture della shell dell'applicazione offrono diversi vantaggi, ma hanno senso solo per alcune classi di applicazioni. Il modello è ancora recente e vale la pena valutare lo sforzo e i vantaggi complessivi in termini di prestazioni di questa architettura.

Nei nostri esperimenti, abbiamo sfruttato la condivisione di modelli tra client e server per ridurre al minimo il lavoro di creazione di due livelli di applicazione. In questo modo, il miglioramento progressivo è ancora una funzionalità principale.

Se stai già prendendo in considerazione l'utilizzo dei service worker nella tua app, dai un'occhiata all'architettura e valuta se ha senso per i tuoi progetti.

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