Portare i Service worker alla Ricerca Google

La storia di ciò che è stato spedito, di come è stato misurato l'impatto e dei compromessi ottenuti.

Contesto

Se cerchi praticamente qualsiasi argomento su Google, visualizzerai una pagina immediatamente riconoscibile con risultati significativi e pertinenti. Quello che probabilmente non sapevi che questa pagina dei risultati di ricerca è, in alcuni casi, servita da una potente tecnologia web chiamata service worker.

L'implementazione del supporto dei Service worker per la Ricerca Google senza influire negativamente sulle prestazioni ha richiesto decine di ingegneri che lavoravano su più team. Questa è la storia di cosa è stato spedito, di come sono state misurate le prestazioni e dei pro e contro.

Motivi principali per l'esplorazione dei service worker

L'aggiunta di un service worker a un'app web, così come qualsiasi modifica all'architettura del tuo sito, deve essere effettuata con una chiara serie di obiettivi. Per il team della Ricerca Google, c'erano alcuni motivi principali per cui valeva la pena di approfondire l'aggiunta di un service worker.

Memorizzazione nella cache dei risultati di ricerca limitata

Il team della Ricerca Google ha rilevato che per gli utenti è frequente cercare gli stessi termini più volte in un breve periodo di tempo. Anziché attivare una nuova richiesta di backend solo per ottenere gli stessi risultati, il team di ricerca ha voluto sfruttare la memorizzazione nella cache e soddisfare le richieste ripetute a livello locale.

L'importanza dell'aggiornamento non può essere scontata e, a volte, gli utenti cercano gli stessi termini più volte perché sono un argomento in continua evoluzione e si aspettano di vedere nuovi risultati. L'utilizzo di un service worker consente al team di ricerca di implementare una logica granulare per controllare la durata dei risultati di ricerca memorizzati nella cache locale e di raggiungere l'equilibrio esatto tra velocità e aggiornamento che ritiene sia di aiuto agli utenti.

Esperienza offline significativa

Inoltre, il team della Ricerca Google voleva offrire un'esperienza offline significativa. Quando un utente vuole trovare informazioni su un argomento, vuole accedere direttamente alla pagina della Ricerca Google e iniziare la ricerca, senza doversi preoccupare di una connessione a internet attiva.

Senza un service worker, la visita alla pagina di ricerca di Google in modalità offline porterebbe solo alla pagina di errore di rete standard del browser e gli utenti avrebbero dovuto ricordare di tornare e riprovare non appena la connessione sarà tornata. Con un service worker, è possibile fornire una risposta HTML offline personalizzata e consentire agli utenti di inserire immediatamente la query di ricerca.

Uno screenshot dell'interfaccia dei nuovi tentativi in background.

I risultati non saranno disponibili finché non sarà disponibile una connessione a internet, ma il service worker consente di posticipare la ricerca e inviarla ai server di Google non appena il dispositivo tornerà online utilizzando l'API di sincronizzazione in background.

Memorizzazione e memorizzazione nella cache di JavaScript in modo più intelligente

Un'altra motivazione è stata l'ottimizzazione della memorizzazione nella cache e del caricamento del codice JavaScript modularizzato su cui si basano i vari tipi di funzionalità nella pagina dei risultati di ricerca. Il raggruppamento di JavaScript offre una serie di vantaggi, in modo da avere senso senza coinvolgere i service worker. Di conseguenza, il team della Ricerca non voleva semplicemente interrompere del tutto il raggruppamento.

Utilizzando la capacità del service worker di creare e memorizzare nella cache blocchi granulari di JavaScript in fase di runtime, il team di ricerca sospettava di poter ridurre la quantità di abbandono della cache e garantire che il codice JavaScript riutilizzato in futuro potesse essere memorizzato nella cache in modo efficiente. La logica all'interno del service worker può analizzare una richiesta HTTP in uscita per un bundle che contiene più moduli JavaScript ed eseguirla unendo più moduli memorizzati nella cache locale, "separando" efficacemente, quando possibile. Ciò consente di risparmiare larghezza di banda dell'utente e migliorare la reattività generale.

Ci sono anche vantaggi in termini di prestazioni dell'utilizzo di JavaScript memorizzato nella cache e fornito da un service worker: in Chrome, viene archiviata e riutilizzata una rappresentazione di codice in byte analizzata di quel codice JavaScript, che viene quindi ridotto in fase di runtime per eseguire il codice JavaScript sulla pagina.

Sfide e soluzioni

Ecco alcuni degli ostacoli da superare per raggiungere gli obiettivi prefissati dal team. Sebbene alcune di queste sfide siano specifiche per la Ricerca Google, molte di queste sono applicabili a un'ampia gamma di siti che potrebbero considerare il deployment di un service worker.

Problema: overhead del service worker

La sfida più grande, nonché l'unico vero ostacolo al lancio di un service worker nella Ricerca Google, era garantire che non facesse nulla che potesse aumentare la latenza percepita dall'utente. La Ricerca Google prende molto le prestazioni e, in passato, ha bloccato i lanci di nuove funzionalità se hanno contribuito anche per decine di millisecondi di latenza aggiuntiva per una determinata popolazione di utenti.

Quando il team ha iniziato a raccogliere dati sul rendimento durante i primi esperimenti, è apparso chiaro che c'era un problema. L'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, il service worker non è in grado di replicare questa logica e restituire immediatamente il codice HTML memorizzato nella cache; il meglio che possa fare è trasferire le richieste di navigazione ai server web di backend, rendendo così necessaria una richiesta di rete.

Senza un service worker, questa richiesta di rete avviene immediatamente al momento della navigazione dell'utente. Quando un service worker è registrato, deve sempre essere avviato e avere la possibilità di eseguire i suoi gestori di eventi fetch, anche quando non esiste alcuna possibilità che questi gestori del recupero facciano qualcosa di diverso dalla rete. Il tempo necessario per l'avvio e l'esecuzione del codice dei worker di servizio equivale all'overhead aggiunto in cima a ogni navigazione:

Illustrazione dell'avvio SW che blocca la richiesta di navigazione.

L'implementazione del service worker comporta uno svantaggio di latenza eccessivo per giustificare altri vantaggi. Inoltre, il team ha scoperto che, sulla base della misurazione dei tempi di avvio dei Service worker su dispositivi reali, i tempi di avvio erano molto diversi e alcuni dispositivi mobili di fascia bassa richiedevano quasi il tempo necessario ad avviare il service worker per fare la richiesta di rete per il codice HTML della pagina dei risultati.

Soluzione: utilizzare il precaricamento della navigazione

L'unica funzionalità più importante che ha consentito al team della Ricerca Google di procedere con il lancio del service worker è stata il precaricamento della navigazione. L'utilizzo del precaricamento di navigazione è una soluzione vincente in termini di prestazioni per qualsiasi service worker che abbia bisogno di utilizzare una risposta dalla rete per soddisfare le richieste di navigazione. Fornisce al browser un suggerimento per iniziare a effettuare immediatamente la richiesta di navigazione, nello stesso momento dell'avvio del service worker:

Illustrazione dell'avvio software eseguito in parallelo alla richiesta di navigazione.

Se il tempo necessario per l'avvio del service worker è inferiore al tempo necessario per ricevere una risposta dalla rete, il service worker non dovrebbe avere un overhead di latenza.

Il team della Ricerca doveva anche evitare di utilizzare un service worker sui dispositivi mobili di fascia bassa in cui il tempo di avvio dei service worker poteva superare la richiesta di navigazione. Poiché non esiste una regola rigida e veloce per determinare ciò che costituisce un dispositivo "di fascia bassa ", è nata l'euristica di controllare la RAM totale installata sul dispositivo. I dispositivi con meno di 2 GB di memoria rientrano nella categoria di dispositivi di fascia bassa, dove il tempo di avvio dei service worker non sarebbe accettabile.

Lo spazio di archiviazione disponibile è un'altra considerazione, poiché l'insieme completo di risorse da memorizzare nella cache per un uso futuro può essere eseguito su diversi megabyte. L'interfaccia di navigator.storage consente alla pagina della Ricerca Google di capire in anticipo se i tentativi di memorizzare i dati nella cache corrono il rischio di non riuscire a causa di errori nella quota di archiviazione.

In questo modo, il team della Ricerca ha potuto utilizzare diversi criteri per stabilire se utilizzare o meno un service worker: se un utente arriva alla pagina della Ricerca Google utilizzando un browser che supporta il precaricamento di navigazione e dispone di almeno 2 gigabyte di RAM e di spazio di archiviazione libero sufficiente, allora viene registrato un service worker. I browser o i dispositivi che non soddisfano questi criteri non finiranno con un worker del servizio, ma potranno usufruire della stessa esperienza di Ricerca Google di sempre.

Un vantaggio secondario di questa registrazione selettiva è la capacità di spedire un service worker più piccolo ed efficiente. Scegliere come target browser abbastanza moderni per eseguire il codice dei service worker elimina l'overhead del processo di traspilazione e polyfill per i browser meno recenti. In questo modo sono state eliminate circa 8 kilobyte di codice JavaScript non compresso dalle dimensioni totali dell'implementazione del service worker.

Problema: ambiti dei service worker

Dopo che il team di ricerca ha eseguito un numero sufficiente di esperimenti di latenza ed era sicuro che l'utilizzo del precaricamento della navigazione offrisse un percorso fattibile e privo di latenza per l'utilizzo di un service worker, alcuni problemi pratici hanno iniziato a essere portati in primo piano. Uno di questi problemi ha a che fare con le regole di ambito dei service worker. L'ambito di un service worker determina su quali pagine può potenzialmente assumere il controllo.

La definizione dell'ambito funziona in base al prefisso del percorso dell'URL. Questo non è un problema per i domini che ospitano una singola app web, poiché in genere si utilizza un service worker con l'ambito massimo di /, che potrebbe assumere il controllo di qualsiasi pagina del dominio. Tuttavia, la struttura dell'URL della Ricerca Google è un po' più complicata.

Se al service worker fosse stato assegnato l'ambito massimo /, finirebbe per essere in grado di prendere il controllo di qualsiasi pagina ospitata in www.google.com (o l'equivalente a livello di regione) e ci sono URL in quel dominio che non hanno nulla a che fare con la Ricerca Google. Un ambito più ragionevole e restrittivo sarebbe /search, che, come minimo, eliminerebbe gli URL completamente non correlati ai risultati di ricerca.

Purtroppo, anche quel 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. Alcune di queste versioni utilizzano codebase completamente diversi rispetto alla tradizionale pagina dei risultati di ricerca web. 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 pronta per fornire la propria esperienza dei service worker (per il momento).

Soluzione: creare un framework di invio e routing

Anche se esistono alcune proposte che consentono qualcosa di più efficace dei prefissi del percorso dell'URL per determinare gli ambiti dei service worker, il team della Ricerca Google era bloccato a eseguire il deployment di un service worker che non ha fatto nulla per un sottoinsieme di pagine che ha controllato.

Per ovviare a questo problema, il team della Ricerca Google ha creato un framework di invio e routing su misura che può essere configurato per verificare criteri come i parametri di query della pagina client e utilizzarli per determinare quale percorso di codice specifico andare verso il basso. Anziché utilizzare regole di hardcoded, il sistema è stato progettato per essere flessibile e consentire ai team che condividono lo spazio dell'URL, come la ricerca immagini e la ricerca di Shopping, di abbandonare la logica del proprio service worker se decidono di implementarla.

Problema: metriche e risultati personalizzati

Gli utenti possono accedere alla Ricerca Google utilizzando i loro Account Google e l'esperienza dei risultati di ricerca può essere personalizzata in base ai dati specifici dell'account. Gli utenti che eseguono l'accesso vengono identificati da cookie del browser specifici, uno standard rispettabile e ampiamente supportato.

Uno svantaggio dell'utilizzo dei cookie del browser, tuttavia, è che non vengono esposti all'interno di un service worker e non è possibile esaminare automaticamente i loro valori e assicurarsi che non siano cambiati a causa della disconnessione di un utente o del cambio di account. (È in corso il lavoro per offrire 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 del service worker dell'utente che ha eseguito l'accesso e quella dell'utente effettivo che ha eseguito l'accesso all'interfaccia web della Ricerca Google potrebbe portare a risultati di ricerca non correttamente personalizzati o a metriche e log non correttamente attribuiti. Ognuno di questi scenari di errore rappresenterebbe un serio problema per il team della Ricerca Google.

Soluzione: inviare cookie usando postMessage

Anziché attendere l'avvio delle API sperimentali e l'accesso diretto ai cookie del browser all'interno di un service worker, il team della Ricerca Google ha optato per una soluzione temporanea: ogni volta che viene caricata una pagina controllata dal service worker, la pagina legge i cookie pertinenti e utilizza postMessage() per inviarli al service worker.

Il service worker quindi controlla il valore del cookie attuale rispetto al valore previsto e, in caso di mancata corrispondenza, prende provvedimenti per eliminare definitivamente i dati specifici dell'utente dal proprio 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 le cose a un livello di base sono specifici dei requisiti della Ricerca Google, ma lo stesso approccio generale può essere utile per altri sviluppatori che si occupano di dati personalizzati associati ai cookie del browser.

Problema: esperimenti e dinamismo

Come accennato, il team della Ricerca Google fa molto affidamento sull'esecuzione di esperimenti in produzione e sui test degli effetti del nuovo codice e delle nuove funzionalità nel mondo reale prima di attivarli per impostazione predefinita. Questo può rappresentare un problema per un service worker statico che fa molto affidamento sui dati memorizzati nella cache, dal momento che l'attivazione e la disattivazione degli esperimenti richiede spesso la comunicazione con il server di backend.

Soluzione: script del service worker generato dinamicamente

La soluzione scelta dal team prevedeva l'utilizzo di uno script del service worker generato dinamicamente, personalizzato dal server web per ogni singolo utente, invece di un singolo script del service worker statico generato in anticipo. Le informazioni sugli esperimenti che potrebbero influire sul comportamento del service worker o sulle richieste di rete in generale sono incluse direttamente negli script personalizzati dei service worker. La modifica degli insiemi di esperienze attive per un utente avviene tramite una combinazione di tecniche tradizionali, come i cookie del browser, oltre alla pubblicazione di codice aggiornato nell'URL del service worker registrato.

L'utilizzo di uno script dei service worker generato dinamicamente semplifica inoltre l'escape hatch nell'improbabile caso in cui l'implementazione di un service worker abbia un bug irreversibile che deve essere evitato. La risposta dinamica del worker server può essere un'implementazione no-op, ovvero la disattivazione del service worker per alcuni o tutti gli utenti correnti.

Problema: coordinamento degli aggiornamenti

Una delle sfide più difficili da affrontare per il deployment dei service worker nel mondo reale è trovare un compromesso ragionevole tra evitare la rete a favore della cache e, allo stesso tempo, garantire che gli utenti esistenti ricevano aggiornamenti e modifiche critici subito dopo il deployment in produzione. Il giusto equilibrio dipende da molti fattori:

  • Indica se la tua app web è un'app a pagina singola di lunga durata che un utente mantiene aperta a tempo indeterminato, senza accedere a nuove pagine.
  • La cadenza di deployment per gli aggiornamenti del server web di backend.
  • Indica se l'utente medio tollererebbe l'utilizzo di una versione leggermente obsoleta dell'app web o se l'aggiornamento è la massima priorità.

Durante gli esperimenti con i service worker, il team della Ricerca Google si è assicurato di mantenere gli esperimenti in esecuzione su una serie di aggiornamenti pianificati del backend, per garantire che le metriche e l'esperienza utente corrispondessero maggiormente a ciò che gli utenti di ritorno avrebbero visto nel mondo reale.

Soluzione: bilancia l'aggiornamento e l'utilizzo della cache

Dopo aver testato una serie di opzioni di configurazione diverse, il team della Ricerca Google ha scoperto che la seguente configurazione forniva il giusto equilibrio tra aggiornamento e utilizzo della cache.

L'URL dello script del service worker viene pubblicato con l'intestazione della risposta Cache-Control: private, max-age=1500 (1500 secondi o 25 minuti) ed è registrato con updateViaCache impostato su "all" per garantire che l'intestazione venga rispettata. Come puoi immaginare, il backend web della Ricerca Google è un grande insieme di server distribuiti a livello globale che richiede un tempo di attività il più vicino possibile al 100%. Il deployment di una modifica che influirebbe sui contenuti dello script del service worker viene eseguito in modo continuativo.

Se un utente accede a un backend che è stato aggiornato e poi passa rapidamente a un'altra pagina che indirizza a un backend che non ha ancora ricevuto il worker di servizio aggiornato, finirebbe per fare il flip-flopping tra le versioni più volte. Pertanto, indicare al browser di preoccuparsi di verificare la presenza di uno script aggiornato solo se sono trascorsi 25 minuti dall'ultimo controllo, non presenta uno svantaggio significativo. L'aspetto positivo dell'attivazione di questo comportamento è una riduzione significativa del traffico ricevuto dall'endpoint che genera dinamicamente lo script del service worker.

Inoltre, sulla risposta HTTP dello script del service worker viene impostata un'intestazione ETag, 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 non sono stati eseguiti aggiornamenti al service worker di cui è stato eseguito il deployment nell'intervallo.

Sebbene alcune interazioni all'interno dell'app web Ricerca Google utilizzino navigazioni in stile app a pagina singola (ovvero tramite l' API History), per la maggior parte, la Ricerca Google è un'app web tradizionale che utilizza navigazioni "reali". Questo entra in gioco quando il team ha deciso che sarebbe stato efficace utilizzare due opzioni che accelerano il ciclo di vita dell'aggiornamento dei service worker: clients.claim() e skipWaiting(). Facendo clic sull'interfaccia della Ricerca Google, in genere si accede a nuovi documenti HTML. La chiamata a skipWaiting garantisce che un service worker aggiornato abbia la possibilità di gestire le nuove richieste di navigazione subito dopo l'installazione. Analogamente, la chiamata a clients.claim() significa che il service worker aggiornato ha la possibilità di iniziare a controllare le pagine aperte della Ricerca Google non controllate dopo l'attivazione del service worker.

L'approccio adottato dalla Ricerca Google non è necessariamente una soluzione adatta a tutti: è il risultato di test A/B accurati di varie combinazioni di opzioni di pubblicazione, fino a quando non hanno trovato quella più adatta alle loro esigenze. Gli sviluppatori la cui infrastruttura di backend consente di eseguire il deployment degli aggiornamenti più rapidamente potrebbero preferire che il browser cerchi uno script del service worker 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, in quanto rischi di riscontrare incoerenze nella cache se consenti l'attivazione del nuovo service worker mentre sono presenti client di lunga durata.

Concetti principali

Per impostazione predefinita, i Service worker non sono neutrali per le prestazioni

Aggiungere un service worker alla tua app web significa inserire un'ulteriore porzione di JavaScript che deve essere caricata ed eseguita prima che l'app web riceva le risposte alle sue richieste. Se quelle risposte provengono da una cache locale anziché dalla rete, l'overhead associato all'esecuzione del service worker è solitamente trascurabile rispetto alla vittoria in termini di prestazioni derivante dal passaggio dalla cache. Tuttavia, se sai che il service worker deve sempre consultare la rete per gestire le richieste di navigazione, l'utilizzo del precaricamento di navigazione è fondamentale per le prestazioni.

I Service worker sono (ancora!) un miglioramento progressivo

La storia dell'assistenza ai service worker è molto più luminosa oggi rispetto a un anno fa. Tutti i browser moderni ora presentano almeno un supporto per i service worker, ma sfortunatamente esistono alcune funzionalità avanzate dei Service worker, come la sincronizzazione in background e il precaricamento della navigazione, che non sono implementate a livello globale. La verifica delle funzionalità per il sottoinsieme specifico di funzionalità di cui si ha bisogno, e la registrazione di un service worker solo quando sono presenti, è comunque un approccio ragionevole.

Analogamente, se hai eseguito esperimenti in natura e sai che i dispositivi di fascia bassa hanno prestazioni scarse con l'overhead aggiuntivo di un service worker, puoi evitare di registrare un service worker anche in questi scenari.

Dovresti continuare a considerare i service worker come un miglioramento progressivo che viene aggiunto a un'app web quando tutti i prerequisiti sono soddisfatti e il worker di servizio aggiunge qualcosa di positivo all'esperienza utente e alle prestazioni di caricamento complessive.

Misura tutto

L'unico modo per capire se la spedizione di un service worker ha avuto un impatto positivo o negativo sull'esperienza degli utenti è sperimentare e misurare i risultati.

Le specifiche dell'impostazione di misurazioni significative dipendono dal fornitore di dati di analisi che utilizzi e da come conduci normalmente gli esperimenti nella configurazione del deployment. Un approccio, che prevede l'utilizzo di Google Analytics per raccogliere le metriche, è descritto in dettaglio in questo case study basato sull'esperienza di utilizzo dei service worker nell'app web Google I/O.

Non obiettivi

Mentre molti membri della community di sviluppo web associano i service worker alle app web progressive, la creazione di una "PWA della Ricerca Google" non era un obiettivo iniziale del team. L'app web Ricerca Google al momento non fornisce metadati tramite un file manifest dell'app web, né incoraggia gli utenti a seguire il flusso Aggiungi alla schermata Home. Al momento il team della Ricerca è soddisfatto del fatto che gli utenti accedano alla sua app web tramite i tradizionali punti di ingresso per la Ricerca Google.

Anziché provare a trasformare l'esperienza web della Ricerca Google nell'equivalente di ciò che ci si aspetta da un'applicazione installata, l'attenzione è stata concentrata sul miglioramento progressivo del sito web esistente.

Ringraziamenti

Ringraziamo tutto il team di sviluppo web della Ricerca Google per il lavoro svolto sull'implementazione dei service worker e per aver condiviso il materiale di base per scrivere 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 in origine, il team della Ricerca Google ha rivalutato i vantaggi e i compromessi dell'attuale architettura dei service worker. Il service worker descritto sopra è in fase di ritiro. Man mano che l'infrastruttura web della Ricerca Google si evolve, il team potrebbe rivedere il design del proprio service worker.