La storia di ciò che è stato spedito, di come è stato misurato l'impatto e dei compromessi fatti.
Sfondo
Cerca quasi qualsiasi argomento su Google e ti verrà presentata una pagina immediatamente riconoscibile di risultati significativi e pertinenti. Probabilmente non sapevi che questa pagina dei risultati di ricerca è, in alcuni casi, gestita da una potente tecnologia web chiamata service worker.
L'implementazione del supporto dei worker di servizio per la Ricerca Google senza influire negativamente sul rendimento ha richiesto decine di ingegneri che lavoravano in più team. Questa è la storia di cosa è stato rilasciato, di come è stato misurato il rendimento e di quali compromessi sono stati fatti.
Motivi principali per esplorare i worker di servizio
L'aggiunta di un service worker a un'app web, così come qualsiasi modifica dell'architettura al tuo sito, deve essere eseguita tenendo presente un insieme chiaro di obiettivi. Per il team della rete di ricerca di Google, l'aggiunta di un worker di servizio era un'opzione da valutare per diversi motivi.
Memorizzazione nella cache dei risultati di ricerca limitata
Il team della Ricerca Google ha scoperto che è normale che gli utenti cerchino gli stessi termini più di una volta in un breve periodo di tempo. Anziché attivare una nuova richiesta di backend solo per ottenere risultati che probabilmente sono gli stessi, il team di Ricerca voleva sfruttare la memorizzazione nella cache e soddisfare queste richieste ripetute localmente.
L'importanza dell'aggiornamento non può essere sottovalutata e, a volte, gli utenti cercano ripetutamente gli stessi termini perché si tratta di un argomento in evoluzione e si aspettano di vedere risultati aggiornati. L'utilizzo di un worker di servizio consente al team della Ricerca di implementare una logica granulare per controllare la durata dei risultati di ricerca memorizzati nella cache locale e raggiungere l'equilibrio esatto tra velocità e aggiornamento che ritiene essere il migliore per gli utenti.
Esperienza offline significativa
Inoltre, il team della Ricerca Google voleva offrire un'esperienza offline significativa. Quando un utente vuole scoprire di più su un argomento, vuole andare direttamente alla pagina della Ricerca Google e iniziare a cercare, senza preoccuparsi di una connessione a internet attiva.
Senza un worker di servizio, la visita alla pagina di ricerca di Google offline rimanderebbe alla pagina di errore di rete standard del browser e gli utenti dovrebbero ricordarsi di tornare e riprovare quando la connessione è tornata disponibile. Con un servizio worker è possibile pubblicare una risposta HTML offline personalizzata e consentire agli utenti di inserire immediatamente la query di ricerca.
I risultati non saranno disponibili finché non sarà presente una connessione a internet, ma il servizio worker consente di posticipare la ricerca e di inviarla ai server di Google non appena il dispositivo torna online utilizzando l'API di sincronizzazione in background.
Cache e pubblicazione di JavaScript più intelligenti
Un'altra motivazione è stata ottimizzare la memorizzazione nella cache e il caricamento del codice JavaScript modularizzato che supporta i vari tipi di funzionalità nella pagina dei risultati di ricerca. Il bundling di JavaScript offre una serie di vantaggi che hanno senso quando non è coinvolto il servizio worker, pertanto il team di Ricerca non voleva semplicemente interrompere completamente il bundling.
Utilizzando la capacità di un worker di servizio di eseguire il versionamento e memorizzare nella cache frammenti granulari di JavaScript in fase di runtime, il team di ricerca sospettava di poter ridurre la quantità di rotazione della cache e garantire che il codice JavaScript riutilizzato in futuro possa essere memorizzato nella cache in modo efficiente. La logica all'interno del proprio worker di servizio può analizzare una richiesta HTTP in uscita per un bundle contenente più moduli JavaScript e soddisfarla riunendo più moduli memorizzati nella cache locale, in pratica "spacchettando" il bundle, se possibile. In questo modo si risparmia la larghezza di banda dell'utente e si migliora la reattività complessiva.
L'utilizzo di JavaScript memorizzato nella cache e servito da un worker di servizio offre anche vantaggi in termini di prestazioni: in Chrome, una rappresentazione in codice byte analizzata di questo JavaScript viene archiviata e riutilizzata, il che riduce il lavoro da svolgere in fase di esecuzione per eseguire il codice JavaScript nella pagina.
Sfide e soluzioni
Ecco alcuni degli ostacoli che è stato necessario superare per raggiungere gli scopi dichiarati del team. Sebbene alcune di queste sfide siano specifiche della Ricerca Google, molte sono applicabili a una vasta gamma di siti che potrebbero prendere in considerazione il deployment di un servizio worker.
Problema: overhead del service worker
La sfida più grande e l'unico vero blocco per il lancio di un worker di servizio su Ricerca Google era assicurarsi che non facesse nulla che potesse aumentare la latenza percepita dall'utente. La Ricerca Google prende molto sul serio il rendimento e, in passato, ha bloccato il lancio di nuove funzionalità se queste contribuivano anche con decine di millisecondi di latenza aggiuntiva per un determinato gruppo di utenti.
Quando il team ha iniziato a raccogliere i dati sul rendimento durante i primi esperimenti, è diventato evidente che si sarebbe verificato un problema. Il codice HTML restituito in risposta alle richieste di navigazione per la pagina dei risultati di ricerca è dinamico e varia notevolmente a seconda della logica che deve essere eseguita sui server web della Ricerca. Al momento non è possibile per il servizio worker replicare questa logica e restituire immediatamente l'HTML memorizzato nella cache. Il meglio che può fare è inoltrare le richieste di navigazione ai server web di backend, il che richiede una richiesta di rete.
Senza un service worker, questa richiesta di rete viene eseguita immediatamente al termine della navigazione dell'utente. Quando un worker del servizio è registrato, deve sempre essere avviato e deve avere la possibilità di eseguire i suoi fetch
gestori eventi, anche quando non c'è alcuna possibilità che questi gestori di recupero facciano altro che accedere alla rete. Il tempo necessario per avviare ed eseguire il codice del worker del servizio è puro overhead aggiunto a ogni navigazione:
Ciò comporta un svantaggio in termini di latenza troppo elevato per giustificare altri vantaggi. Inoltre, il team ha scoperto che, sulla base della misurazione dei tempi di avvio dei worker di servizio su dispositivi reali, esisteva una distribuzione ampia dei tempi di avvio, con alcuni dispositivi mobili di fascia bassa che impiegavano quasi lo stesso tempo per avviare il worker di servizio necessario per effettuare la richiesta di rete per l'HTML della pagina dei risultati.
Soluzione: utilizza il precaricamento della navigazione
La singola funzionalità più importante che ha consentito al team della Ricerca Google di procedere con il lancio del proprio service worker è il precaricamento della navigazione. L'utilizzo del precaricamento della navigazione è un vantaggio fondamentale per le prestazioni di qualsiasi worker di servizio che deve utilizzare una risposta della rete per soddisfare le richieste di navigazione. Fornisce un suggerimento al browser per iniziare a inviare la richiesta di navigazione immediatamente, contemporaneamente all'avvio del service worker:
Se il tempo necessario per l'avvio del service worker è inferiore al tempo necessario per ricevere una risposta dalla rete, non dovrebbe esserci alcun sovraccarico di latenza introdotto dal service worker.
Il team di Ricerca doveva anche evitare di utilizzare un servizio worker su dispositivi mobili di fascia bassa, in cui il tempo di avvio del servizio worker potrebbe superare la richiesta di navigazione. Poiché non esiste una regola fissa per stabilire cosa costituisce un dispositivo "low-end ", è stata ideata l'euristica di controllare la RAM totale installata sul dispositivo. Qualsiasi quantità di memoria inferiore a 2 gigabyte rientrava nella loro categoria di dispositivi di fascia bassa, in cui il tempo di avvio del servizio worker sarebbe stato inaccettabile.
Un altro aspetto da considerare è lo spazio di archiviazione disponibile, poiché l'insieme completo di risorse da memorizzare nella cache per un uso futuro può occupare diversi megabyte. L'interfaccia navigator.storage
consente alla pagina Ricerca Google di capire in anticipo se i tentativi di memorizzazione nella cache dei dati rischiano di non riuscire a causa di errori relativi alla quota di spazio di archiviazione.
Il team della Ricerca ha quindi a disposizione diversi criteri per determinare se utilizzare o meno un worker di servizio: se un utente arriva alla pagina della Ricerca Google utilizzando un browser che supporta il precaricamento della navigazione e dispone di almeno 2 gigabyte di RAM e di spazio di archiviazione libero sufficiente, un worker di servizio viene registrato. I browser o i dispositivi che non soddisfano questi criteri non avranno un servizio worker, ma continueranno a visualizzare la stessa esperienza della Ricerca Google di sempre.
Un vantaggio secondario di questa registrazione selettiva è la possibilità di pubblicare un servizio worker più piccolo ed efficiente. Scegliere come target browser abbastanza recenti per eseguire il codice del servizio worker elimina il sovraccarico della transpilazione e dei polyfill per i browser meno recenti. In questo modo, sono stati eliminati circa 8 kilobyte di codice JavaScript non compresso dalle dimensioni totali dell'implementazione del servizio worker.
Problema: ambiti dei service worker
Una volta che il team di ricerca ha eseguito esperimenti sulla latenza sufficienti ed è stato certo che l'utilizzo del precaricamento della navigazione offriva un percorso fattibile e incentrato sulla latenza per l'utilizzo di un servizio worker, alcuni problemi pratici hanno iniziato a emergere. Uno di questi problemi riguarda le regole di ambito dei worker di servizio. L'ambito di un service worker determina le pagine di cui può potenzialmente assumere il controllo.
L'ambito funziona in base al prefisso del percorso dell'URL. Per i domini che ospitano una singola app web, questo non è un problema, in quanto in genere utilizzi un service worker con l'ambito massimo di /
, che può assumere il controllo di qualsiasi pagina all'interno del dominio.
Tuttavia, la struttura degli URL della Ricerca Google è un po' più complicata.
Se al service worker venisse assegnato l'ambito massimo di /
, potrebbe assumere il controllo di qualsiasi pagina ospitata in /
(o nell'equivalente regionale) e ci sono URL in quel dominio che non hanno nulla a che fare con la Ricerca Google.www.google.com
Un ambito più ragionevole e restrittivo sarebbe /search
, che almeno eliminerebbe gli URL completamente estranei ai risultati di ricerca.
Purtroppo, anche il percorso dell'URL /search
è condiviso tra diversi tipi di risultati della Ricerca Google, con i parametri di query dell'URL che determinano quale tipo specifico di risultato di ricerca viene mostrato. Alcuni di questi tipi utilizzano codebase completamente diverse rispetto alla pagina dei risultati di ricerca web tradizionale. Ad esempio, la Ricerca immagini
e la Ricerca Shopping vengono entrambe pubblicate nel percorso dell'URL /search
con parametri di query diversi, ma nessuna di queste interfacce era ancora pronta per implementare la propria esperienza con i worker di servizio.
Soluzione: crea un framework di invio e routing
Sebbene esistano alcune proposte che consentono di utilizzare qualcosa di più potente dei prefissi dei percorsi dell'URL per determinare gli ambiti dei worker di servizio, il team della Ricerca Google non riusciva a implementare un worker di servizio che non faceva nulla per un sottoinsieme di pagine che controllava.
Per risolvere il problema, il team di Ricerca Google ha creato un framework di invio e routing personalizzato che può essere configurato per verificare criteri come i parametri di query della pagina del cliente e utilizzarli per determinare quale percorso di codice specifico seguire. Anziché codificare le regole in modo rigido, il sistema è stato progettato per essere flessibile e consentire ai team che condividono lo spazio URL, come la Ricerca immagini e la Ricerca Shopping, di inserire in un secondo momento la propria logica di worker di servizio, se scelgono di implementarla.
Problema: risultati e metriche personalizzati
Gli utenti possono accedere alla Ricerca Google utilizzando i propri Account Google e la loro esperienza con i risultati di ricerca può essere personalizzata in base ai dati specifici del loro account. Gli utenti che hanno eseguito l'accesso vengono identificati da cookie del browser specifici, che sono uno standard affidabile e ampiamente supportato.
Un aspetto negativo dell'utilizzo dei cookie del browser, però, è che non sono esposti all'interno di un service worker e non è possibile esaminarne automaticamente i valori e assicurarsi che non siano cambiati a causa della disconnessione di un utente o del cambio di account. Sono in corso sforzi per consentire l'accesso ai cookie ai service worker, ma al momento della stesura di questo articolo, l'approccio è sperimentale e non è ampiamente supportato.
Una mancata corrispondenza tra la visualizzazione dell'utente attualmente connesso del service worker e l'utente effettivo che ha eseguito l'accesso all'interfaccia web della Ricerca Google potrebbe portare a risultati di ricerca personalizzati in modo errato o a metriche e log attribuiti erroneamente. Ognuno di questi scenari di errore rappresenterebbe un problema serio per il team della Rete di ricerca di Google.
Soluzione: invia i cookie utilizzando postMessage
Anziché attendere il lancio delle API sperimentali e fornire l'accesso diretto ai cookie del browser all'interno di un worker di servizio, il team della Ricerca Google ha optato per una soluzione temporanea: ogni volta che viene caricata una pagina controllata dal worker di servizio, la pagina legge i cookie pertinenti e utilizza postMessage()
per inviarli al worker di servizio.
Il service worker controlla quindi il valore corrente del cookie rispetto al valore previsto e, in caso di mancata corrispondenza, adotta misure per eliminare eventuali dati specifici dell'utente dallo spazio di archiviazione e ricarica la pagina dei risultati di ricerca senza alcuna personalizzazione errata.
I passaggi specifici che il service worker esegue per reimpostare i valori su un valore di riferimento sono specifici dei requisiti di Ricerca Google, ma lo stesso approccio generale potrebbe essere utile ad altri sviluppatori che si occupano di dati personalizzati basati su cookie dei browser.
Problema: esperimenti e dinamismo
Come accennato, il team della Ricerca Google si basa molto sull'esecuzione di esperimenti in produzione e sul test degli effetti del nuovo codice e delle nuove funzionalità nel mondo reale prima di attivarli per impostazione predefinita. Questo può essere un po' complicato con un servizio worker statico che si basa molto sui dati memorizzati nella cache, poiché l'attivazione e la disattivazione degli esperimenti da parte degli utenti spesso richiede la comunicazione con il server di backend.
Soluzione: script del servizio worker generato dinamicamente
La soluzione scelta dal team è stata quella di utilizzare uno script di worker di servizio generato dinamicamente, personalizzato dal server web per ogni singolo utente, anziché un singolo script di worker di servizio statico generato in anticipo. Le informazioni sugli esperimenti che potrebbero influire sul comportamento del servizio worker o sulle richieste di rete in generale sono incluse direttamente in questi script del servizio worker personalizzati. La modifica degli insiemi di esperienze attive per un utente viene eseguita tramite una combinazione di tecniche tradizionali, come i cookie del browser, nonché l'invio di codice aggiornato nell'URL del service worker registrato.
L'utilizzo di uno script di worker di servizio generato dinamicamente semplifica anche la fornitura di una via di fuga nell'improbabile caso in cui un'implementazione di worker di servizio presenti un bug fatale da evitare. La risposta del servizio worker dinamico potrebbe essere un'implementazione no-op, che disattiva efficacemente il servizio worker per alcuni o tutti gli utenti attuali.
Problema: coordinamento degli aggiornamenti
Una delle sfide più difficili che si incontrano durante il deployment di un servizio worker reale è trovare un compromesso ragionevole tra l'evitare la rete a favore della cache e, allo stesso tempo, garantire che gli utenti esistenti ricevano aggiornamenti e modifiche critici poco dopo il deployment in produzione. Il giusto equilibrio dipende da molti fattori:
- Se la tua app web è una app a pagina singola di lunga durata che un utente mantiene aperta a tempo indeterminato, senza passare a nuove pagine.
- La cadenza di deployment per gli aggiornamenti del server web di backend.
- Se l'utente medio tollererebbe l'utilizzo di una versione leggermente obsoleta della tua app web o se la novità è la massima priorità.
Durante gli esperimenti con i service worker, il team della Ricerca Google si è impegnato a mantenere in esecuzione gli esperimenti su una serie di aggiornamenti di backend pianificati per garantire che le metriche e l'esperienza utente corrispondessero più da vicino a ciò che gli utenti di ritorno vedrebbero nella realtà.
Soluzione: trova un equilibrio tra aggiornamento e utilizzo della cache
Dopo aver testato diverse opzioni di configurazione, il team di Ricerca Google ha riscontrato che la seguente configurazione offriva il giusto equilibrio tra aggiornamento e utilizzo della cache.
L'URL dello script del service worker viene pubblicato con l'intestazione di risposta Cache-Control: private, max-age=1500
(1500 secondi o 25 minuti) e viene registrato con updateViaCache impostato su "all" per garantire che l'intestazione venga rispettata. Il backend web della Ricerca Google è, come puoi immaginare, un insieme di server di grandi dimensioni distribuiti a livello globale che richiede un tempo di attività il più vicino possibile al 100%. Il deployment di una modifica che influisca sui contenuti dello script del servizio worker viene eseguito in modo incrementale.
Se un utente accede a un backend aggiornato e poi passa rapidamente a un'altra pagina che accede a un backend che non ha ancora ricevuto il servizio aggiornato, finirà per passare da una versione all'altra più volte. Pertanto, dire al browser di controllare la presenza di uno script aggiornato solo se sono trascorsi 25 minuti dall'ultimo controllo non ha svantaggi significativi. Il vantaggio dell'attivazione di questo comportamento è la riduzione significativa del traffico ricevuto dall'endpoint che genera dinamicamente lo script del worker di servizio.
Inoltre, viene impostata un'intestazione ETag sulla risposta HTTP dello script del servizio worker, in modo che, quando viene eseguito un controllo degli aggiornamenti dopo 25 minuti, il server possa rispondere in modo efficiente con una risposta HTTP 304 se nel frattempo non sono stati apportati aggiornamenti al servizio worker di cui è stato eseguito il deployment.
Sebbene alcune interazioni all'interno dell'app web Ricerca Google utilizzino navigazioni simili a quelle delle app a pagina singola (ad es. tramite l'API History), per la maggior parte la Ricerca Google è un'app web tradizionale che utilizza navigazioni "reali". Questo viene utilizzato quando il team ha deciso che sarebbe stato efficace utilizzare due opzioni che accelerano il ciclo di vita dell'aggiornamento dei worker di servizio: clients.claim()
e skipWaiting()
.
In genere, facendo clic sull'interfaccia della Ricerca Google si accede a nuovi documenti HTML. La chiamata a skipWaiting
garantisce che un worker di servizio aggiornato abbia la possibilità di gestire le nuove richieste di navigazione immediatamente dopo l'installazione. Analogamente, l'attivazione di clients.claim()
consente al servizio worker aggiornato di iniziare a controllare le pagine della Ricerca Google aperte non controllate dopo l'attivazione del servizio worker.
L'approccio adottato dalla Ricerca Google non è necessariamente una soluzione che funziona per tutti: è il risultato di test A/B accurati su varie combinazioni di opzioni di pubblicazione fino a trovare quella più adatta.
Gli sviluppatori la cui infrastruttura di backend consente di implementare gli aggiornamenti più rapidamente potrebbero preferire che il browser verifichi la presenza di uno script di worker di servizio aggiornato il più spesso possibile, ignorando sempre la cache HTTP.
Se stai creando un'app a pagina singola che gli utenti potrebbero tenere aperta per un lungo periodo di tempo, l'utilizzo di skipWaiting()
probabilmente non è la scelta giusta per te: rischi di riscontrare incoerenze nella cache se consenti l'attivazione del nuovo service worker quando sono presenti client di lunga durata.
Concetti principali
Per impostazione predefinita, i worker di servizio non sono indipendenti dalle prestazioni
L'aggiunta di un service worker alla tua app web comporta l'inserimento di un ulteriore frammento di codice JavaScript che deve essere caricato ed eseguito prima che l'app web riceva risposte alle sue richieste. Se queste risposte provengono da una cache locale piuttosto che dalla rete, il sovraccarico dell'esecuzione del worker di servizio è in genere trascurabile rispetto al miglioramento delle prestazioni ottenuto con l'approccio cache-first. Tuttavia, se sai che il tuo service worker deve sempre consultare la rete per gestire le richieste di navigazione, l'utilizzo del precaricamento della navigazione è un vantaggio cruciale per le prestazioni.
I service worker sono (ancora) un miglioramento progressivo
La situazione relativa all'assistenza per i worker di servizio è oggi molto più rosea rispetto a un anno fa. Tutti i browser moderni ora offrono almeno un minimo di supporto per i worker di servizio, ma purtroppo alcune funzionalità avanzate dei worker di servizio, come la sincronizzazione in background e il precaricamento della navigazione, non sono implementate universalmente. Il controllo delle funzionalità per il sottoinsieme specifico di funzionalità che sai di aver bisogno e la registrazione di un servizio worker solo quando sono presenti è ancora un approccio ragionevole.
Analogamente, se hai eseguito esperimenti in produzione e sai che i dispositivi di fascia bassa hanno un rendimento scadente con il sovraccarico aggiuntivo di un servizio worker, puoi astenerti dal registrarne uno anche in questi scenari.
Devi continuare a trattare i worker come un miglioramento progressivo che viene aggiunto a un'app web quando sono soddisfatti tutti i prerequisiti e il worker aggiunge qualcosa di positivo all'esperienza utente e al rendimento complessivo del caricamento.
Misurare tutto
L'unico modo per capire se l'invio di un worker di servizio ha avuto un impatto positivo o negativo sulle esperienze degli utenti è fare esperimenti e misurare i risultati.
I dettagli della configurazione di misurazioni significative dipendono dal fornitore di analisi che utilizzi e da come conduci normalmente gli esperimenti nella configurazione di implementazione. Un approccio, che utilizza Google Analytics per raccogliere le metriche, è descritto in dettaglio in questo caso di studio, basato sull'esperienza con i service worker nell'app web Google I/O.
Non obiettivi
Sebbene molti nella community di sviluppo web associno i service worker alle app web progressive, la creazione di una "PWA di Ricerca Google" non era un obiettivo iniziale del team. Al momento l'applicazione web della Ricerca Google non fornisce metadati tramite un manifest dell'app web né incoraggia gli utenti a seguire il percorso Aggiungi alla schermata Home. Al momento il team della Ricerca è soddisfatto del numero di utenti che raggiungono la propria app web tramite i punti di contatto tradizionali della Ricerca Google.
Anziché cercare di trasformare l'esperienza web della Ricerca Google nell'equivalente di ciò che ti aspetteresti da un'applicazione installata, l'obiettivo dell'implementazione iniziale era migliorare progressivamente il sito web esistente.
Ringraziamenti
Grazie all'intero team di sviluppo web della Ricerca Google per il lavoro svolto sull'implementazione dei worker di servizio e per aver condiviso il materiale di riferimento utilizzato per la stesura di questo articolo. Un ringraziamento particolare a Philippe Golle, Rajesh Jagannathan, R. Samuel Klatchko, Andy Martone, Leonardo Peña, Rachel Shearer, Greg Terrono e Clay Woolam.
Aggiornamento (ottobre 2021): da quando questo articolo è stato pubblicato per la prima volta, il team della Ricerca Google ha rivalutato i vantaggi e i compromessi dell'attuale architettura dei worker di servizio. Il service worker descritto sopra verrà ritirato. Con l'evoluzione dell'infrastruttura web della Ricerca Google, il team potrebbe rivedere il design del service worker.