In che modo lo streaming HTTP può migliorare le prestazioni della pagina e come Airbnb lo ha abilitato su una base di codice esistente
Di: Vittorio Lin
Potresti aver sentito una battuta che il Internet è una serie di tubi. In questo post del blog, parleremo di come ottenere un flusso fresco e rinfrescante di byte Airbnb.com nel tuo browser il più rapidamente possibile utilizzando lo streaming HTTP.
Prima di tutto capiamo cosa significa streaming. Immagina di avere un rubinetto e due opzioni:
- Riempi una tazza grande e poi versa tutto nel tubo (la strategia “tamponata”)
- Collegare il rubinetto direttamente al tubo (la strategia “streaming”)
Nella strategia con buffer, tutto avviene in sequenza: i nostri server prima generano l’intera risposta in un buffer (riempiendo la tazza), quindi viene impiegato più tempo per inviarla sulla rete (versandola). La strategia di streaming avviene in parallelo. Suddividiamo la risposta in blocchi, che vengono inviati non appena sono pronti. Il server può iniziare a lavorare sul blocco successivo mentre i blocchi precedenti sono ancora in fase di invio e il client (ad esempio un browser) può iniziare a gestire la risposta prima che sia stata completamente ricevuta.
Lo streaming ha chiari vantaggi, ma la maggior parte dei siti web oggi fa ancora affidamento su un approccio bufferizzato per generare risposte. Uno dei motivi è lo sforzo ingegneristico aggiuntivo necessario per suddividere la pagina in blocchi indipendenti. Questo non è fattibile a volte. Ad esempio, se tutto il contenuto della pagina si basa su una query back-end lenta, non saremo in grado di inviare nulla fino al termine della query.
Tuttavia, esiste un caso d’uso universalmente applicabile. Possiamo usare lo streaming per ridurre cascate di rete. Questo termine si riferisce a quando una richiesta di rete ne attiva un’altra, risultando in una serie a cascata di richieste sequenziali. Questo è facilmente visualizzabile in uno strumento come quello di Chrome Cascata:
La maggior parte delle pagine Web si basa su file JavaScript e CSS esterni collegati all’interno dell’HTML, con conseguente cascata di rete: il download dell’HTML attiva i download di JavaScript e CSS. Di conseguenza, è consigliabile posizionare tutti i tag CSS e JavaScript vicino all’inizio dell’HTML nel file <head>
etichetta. Ciò garantisce che il browser li veda prima. Con lo streaming, possiamo ridurre ulteriormente questo ritardo, inviando quella parte del file <head>
etichetta prima.
Il modo più semplice per inviare un anticipo <head>
tag consiste nel suddividere una risposta standard in due parti. Questa tecnica è chiamata Colore anticipatopoiché una parte viene inviata (“lavata”) prima dell’altra.
La prima parte contiene cose che sono veloci da calcolare e possono essere inviate rapidamente. In Airbnb, includiamo tag per caratteri, CSS e JavaScript, in modo da ottenere i vantaggi del browser sopra menzionati. La seconda parte contiene il resto della pagina, incluso il contenuto che si basa su API o query di database per il calcolo. Il risultato finale è simile a questo:
Primo pezzo:
<html>
<head>
<script src=… defer />
<link rel=”stylesheet” href=… />
<!--lots of other <meta> and other tags… -
Pezzo tardivo:
<!-- <head> tags that depend on data go here ->
</head>
<body>
<! — Body content here →
</body>
</html>
Abbiamo dovuto ristrutturare la nostra app per renderlo possibile. Per il contesto, Airbnb utilizza un server NodeJS basato su Express per eseguire il rendering delle pagine Web utilizzando React. In precedenza avevamo un singolo componente React incaricato di rendere il documento HTML completo. Tuttavia, ciò presentava due problemi:
- Produrre blocchi incrementali di contenuto significa che dobbiamo lavorare con tag HTML parziali/non chiusi. Ad esempio, gli esempi che hai visto sopra sono HTML non validi. IL
<html>
E<head>
i tag vengono aperti nel blocco Early, ma chiusi nel blocco Late. Non c’è modo di generare questo tipo di output utilizzando le funzioni di rendering standard di React. - Non possiamo eseguire il rendering di questo componente finché non abbiamo tutti i dati per esso.
Abbiamo risolto questi problemi suddividendo il nostro componente monolitico in tre:
- un componente “Early”.
- un componente “Late”, per i tagche dipendono dai dati
- un componente “”.
Ogni componente esegue il rendering del file Contenuti dell’etichetta della testa o del corpo. Quindi li uniamo insieme scrivendo tag di apertura/chiusura direttamente nel flusso di risposta HTTP. Nel complesso, il processo è simile a questo:
- Scrivere
<html><head>
- Renderizza e scrivi Earlyalla risposta
- Aspetta i dati
- Renderizza e scrivi il Latealla risposta
- Scrivere
</head><body>
- Renderizza e scrivinella risposta
- Termina scrivendo
</body></html>
Early Flush ottimizza le cascate di rete CSS e JavaScript. Tuttavia, gli utenti continueranno a fissare una pagina vuota fino al <body>
arriva il cartellino. Vorremmo migliorare questo rendendo uno stato di caricamento quando non ci sono dati, che viene sostituito una volta che i dati arrivano. Convenientemente, abbiamo già stati di caricamento in questa situazione per il routing lato client, quindi potremmo farlo semplicemente eseguendo il rendering dell’app senza attendere i dati!
Sfortunatamente, questo causa un’altra cascata di rete. I browser devono ricevere l’SSR (Server-Side Render), quindi JavaScript attiva un’altra richiesta di rete per recuperare i dati effettivi:
Nei nostri test, questo ha comportato un rallentamento totale tempo di caricamento.
E se potessimo includere questi dati nell’HTML? Ciò consentirebbe il nostro rendering lato server e il recupero dei dati in parallelo:
Dato che avevamo già suddiviso la pagina in due blocchi con Early Flush, è relativamente semplice introdurre un terzo blocco per ciò che chiamiamo Dati differiti. Questo blocco segue tutto il contenuto visibile e non blocca il rendering. Eseguiamo le richieste di rete sul server e trasmettiamo le risposte nel blocco dei dati differiti. Alla fine, i nostri tre pezzi hanno questo aspetto:
Primo pezzo
<html>
<head>
<link rel=”preload” as=”script” href=… />
<link rel=”stylesheet” href=… />
<! — lots of other <meta> and other tags… →
Pezzo di corpo
<! — <head> tags that depend on data go here →
</head>
<body>
<! — Body content here →
<script src=… />
Blocco di dati differiti
<script type=”application/json” >
<!-- data -->
</script>
</body>
</html>
Con questo implementato sul server, l’unica attività rimanente è scrivere un codice JavaScript per rilevare quando arriva il nostro blocco di dati differiti. Lo abbiamo fatto con a Osservatore di mutazione, che è un modo efficiente per osservare le modifiche al DOM. Una volta rilevato l’elemento Deferred Data JSON, analizziamo il risultato e lo inseriamo nell’archivio dati di rete della nostra applicazione. Dal punto di vista dell’applicazione, è come se fosse stata completata una normale richiesta di rete.
Fai attenzione al “differimento”.
Potresti notare che alcuni tag vengono riordinati dall’esempio Early Flush. I tag di script sono stati spostati dal blocco Early al blocco Body e non hanno più l’estensione rimandare l’attributo. Questo attributo evita l’esecuzione di script che bloccano il rendering rinviando gli script fino a dopo che l’HTML è stato scaricato e analizzato. Questo non è ottimale quando si utilizzano i dati differiti, poiché tutto il contenuto visibile è già stato ricevuto entro la fine del blocco del corpo e a quel punto non ci preoccupiamo più del blocco del rendering. Possiamo risolvere questo problema spostando i tag dello script alla fine del blocco Body e rimuovendo l’attributo defer. Lo spostamento dei tag più avanti nel documento introduce una rete a cascata, che abbiamo risolto aggiungendo precarico tag nel blocco Early.
Early Flush impedisce modifiche successive alle intestazioni (ad esempio per reindirizzare o modificare il codice di stato). Nel mondo React + NodeJS, è comune delegare i reindirizzamenti e il lancio di errori a un’app React resa dopo che i dati sono stati recuperati. Questo non funzionerà se hai già inviato un messaggio in anticipo <head>
tag e uno stato 200 OK.
Abbiamo risolto questo problema spostando l’errore e reindirizzando la logica fuori dalla nostra app React. Quella logica è ora eseguita Middleware del server espresso prima di tentare l’Early Flush.
L’abbiamo trovato nginx bufferizza le risposte per impostazione predefinita. Ciò ha vantaggi nell’utilizzo delle risorse ma è controproducente quando l’obiettivo è inviare risposte incrementali. Abbiamo dovuto configurare questi servizi per disabilitare il buffering. Ci aspettavamo un potenziale aumento dell’utilizzo delle risorse con questa modifica, ma l’impatto è stato trascurabile.
Abbiamo notato che le nostre risposte Early Flush avevano un ritardo inaspettato di circa 200 ms, che è scomparso quando abbiamo disabilitato la compressione gzip. Questa si è rivelata un’interazione tra Algoritmo di Nagle E ACK ritardato. Queste ottimizzazioni tentano di massimizzare i dati inviati per pacchetto, introducendo latenza durante l’invio di piccole quantità di dati. È particolarmente facile imbattersi in questo problema cornici enormi, che aumenta le dimensioni massime dei pacchetti. Si scopre che gzip ha ridotto la dimensione delle nostre scritture al punto in cui non potevano riempire un pacchetto e la soluzione era disabilitare l’algoritmo di Nagle nel nostro haproxy bilanciamento del carico.
Lo streaming HTTP è stata una strategia di grande successo per migliorare le prestazioni web di Airbnb. I nostri esperimenti hanno dimostrato che Early Flush ha prodotto una riduzione piatta Prima vernice contenta (FCP) di circa 100 ms su ogni pagina testata, inclusa la home page di Airbnb. Lo streaming dei dati ha ulteriormente eliminato i costi FCP delle lente query back-end. Sebbene ci fossero delle sfide lungo il percorso, abbiamo scoperto che l’adattamento della nostra applicazione React esistente per supportare lo streaming era molto fattibile e robusto, nonostante non fosse stato progettato originariamente per questo. Siamo anche entusiasti di vedere la più ampia tendenza dell’ecosistema frontend nella direzione di dare priorità allo streaming, da @defer e @stream in GraphQL A streaming SSR in Next.js. Sia che tu stia utilizzando queste nuove tecnologie o estendendo una base di codice esistente, ci auguriamo che esplorerai lo streaming per creare un frontend più veloce per tutti!
Se questo tipo di lavoro ti interessa, dai un’occhiata ad alcune delle nostre posizioni correlate Qui.
Elliott Sprehn, Aditya Punjani, Jason Jian, Changgeng Li, Siyuan Zhou, Bruce Paul, Max Sadrieh e tutti gli altri che hanno contribuito a progettare e implementare lo streaming su Airbnb!
Tutti i nomi di prodotti, loghi e marchi sono di proprietà dei rispettivi proprietari. Tutti i nomi di società, prodotti e servizi utilizzati in questo sito Web sono solo a scopo identificativo. L’uso di questi nomi, loghi e marchi non implica l’approvazione.