Nel corso degli ultimi dieci anni, l’ottimizzazione della performance web è stata controllata da una linea guida indiscutibile: la miglior richiesta è nessuna richiesta. Una regola molto semplice, facile da interpretare: l’eliminazione di ogni chiamata di rete per una risorsa migliora la performance. Ogni attributo src
risparmiato, ogni elemento link
eliminato. Ma tutto è cambiato ora che abbiamo a disposizione HTTP/2, vero? Progettato per il web moderno, HTTP/2 è più efficiente nel rispondere a un gran numero di richieste rispetto al suo predecessore. Quindi la domanda è: vale ancora la vecchia regola del ridurre le request?
Cosa è cambiato con HTTP/2?#section1
Per capire in che modo HTTP/2 sia diverso, ci è utile conoscere i suoi predecessori. Eccone una breve storia. HTTP è costruito su TCP. Mentre TCP è potente ed è in grado di trasferire molti dati in maniera affidabile, il modo in cui HTTP/1 utilizzava TCP era inefficiente. Ogni risorsa richiesta richiedeva una nuova connessione TCP. E ogni connessione TCP richiedeva la sincronizzazione tra il client e il server, risultando in un ritardo iniziale mentre il browser stabiliva una connessione. Questo andava bene nel momento in cui la maggioranza dei contenuti dei siti web consisteva di documenti senza stili che non caricavano risorse aggiuntive, come le immagini o i file JavaScript.
Gli aggiornamenti in HTTP/1.1 hanno cercato di superare questi limiti. I client erano in grado di usare una connessione TCP per risorse multiple, ma dovevano ancora scaricarle in sequenza. La cosiddetta “head of line blocking” fa apparire i grafici a cascata esattamente come delle cascate:

Figura 1. Schema a cascata di risorse che si caricano su una connessione TCP pipelined
Inoltre, la maggior parte dei browser ha cominciato ad aprire connessioni TCP multiple in parallelo, limitate a un numero piuttosto basso per dominio. Anche con tali ottimizzazioni, HTTP/1.1 non è adatto al numero considerevole di risorse degli odierni siti web. Da qui il detto “la miglior richiesta è nessuna richiesta”. Le connessioni TCP sono costose e richiedono tempo. Questo è il motivo per cui usiamo cose come la concatenazione, le image sprite e l’inlining delle risorse: per evitare nuove connessioni e riutilizzare quelle esistenti.
HTTP/2 è fondamentalmente diverso da HTTP/1.1. HTTP/2 usa una singola connessione TCP e permette a più risorse di essere scaricate in parallelo rispetto ai suoi predecessori. Pensate a questa singola connessione TCP come a un tunnel largo in cui i dati vengono inviati in frame. Sul client, tutti i pacchetti vengono riassemblati nel loro sorgente originale. Usando una paio di elementi link
per trasferire i fogli di stile è ora praticamente tanto efficiente quanto tutti i vostri fogli di stile in un unico file.

Figura 2. Schema a cascata di asset che si caricano su una connessione TCP condivisa
Tutte le connessioni usano lo stesso stream, quindi condividono anche la banda. A seconda del numero di risorse, questo potrebbe significare che le risorse individuali potrebbero metterci di più ad essere trasmesse al lato client su connessioni con poca banda.
Ciò significa anche che l’assegnazione delle priorità alle risorse non è fatta così facilmente come con HTTP/1.1: l’ordine delle risorse nel documento ha un impatto sull’ordine di download. Con HTTP/2, tutto accade allo stesso tempo! La specifica di HTTP/2 contiene informazioni sull’assegnazione di priorità allo stream, ma nel momento in cui scrivo, fa ancora parte di un futuro remoto il mettere nelle mani degli sviluppatori il controllo sull’assegnazione di priorità.
La miglior richiesta è nessuna richiesta: scegliere selettivamente#section2
Allora, cosa possiamo fare per superare la mancanza di priorità delle risorse a cascata? Cosa ne dite del non sprecare banda? Pensate ancora alla prima regola dell’ottimizzazione della performance: la miglior richiesta è nessuna richiesta. Reinterpretiamo questa regola.
Per esempio, consideriamo una tipica pagina web (in questo caso, da Dynatrace). Lo screenshot qui sotto mostra un pezzo di documentazione online consistente di diversi componenti: la navigazione principale, un footer, dei breadcrumb, una sidebar e l’articolo principale.

Figura 3. Un tipico sito web scomposto in alcune sue componenti
Su altre pagine dello stesso sito abbiamo cose come una testata, i social media outlet, le gallery o altre componenti. Ciascuna delle quali è definita dal suo markup e dal suo foglio di stile.
Negli ambienti HTTP/1.1, combineremmo tipicamente tutti i fogli di stile delle componenti in un unico file CSS. La miglior richiesta è nessuna richiesta: una connessione TCP per trasferire tutto il CSS necessario, anche per le pagine che l’utente non ha ancora visto. Questo può risultare in un enorme file CSS.
Il problema è aggravato quando un sito usa una libreria come Bootstrap, che ha raggiunto la soglia dei 300 kB, aggiungendo a questa del CSS specifico per il sito. La quantità effettiva di CSS richiesto da ogni pagina, in alcuni casi, era addirittura meno del 10% della quantità caricata:

Figura 4. Copertura del codice di una pagina web di cinema a caso che usa il 10% del totale di 300 kB di CSS. Questa pagina è costruita su Bootstrap.
Ci sono addirittura tool come UnCSS che hanno lo scopo di eliminare gli stili inutilizzati.
L’esempio della documentazione di Dynatrace mostrato in figura 3 è costruito con la libreria di stili propria dell’azienda, creata su misura per i bisogni specifici del sito, invece che con Bootstrap, che è una soluzione general purpose. Tutti i componenti nella style library dell’azienda sommati arrivano a 80 kB di CSS. Il CSS effettivamente usato sulla pagina è diviso tra otto di quei componenti, per un totale di 8.1 kB. Quindi, anche se la libreria è su misura per gli specifici bisogni del sito, la pagina usa ancora solo circa il 10% del CSS che scarica.
HTTP/2 ci permette di essere molto più esigenti in termini di file che vogliamo trasmettere. La richiesta stessa non è così costosa come lo è in HTTP/1.1, quindi possiamo tranquillamente usare più elementi link
, puntando direttamente agli elementi usati in quella particolare pagina:
<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/typography.css">
<link rel="stylesheet" href="/css/layout.css">
<link rel="stylesheet" href="/css/navbar.css">
<link rel="stylesheet" href="/css/article.css">
<link rel="stylesheet" href="/css/footer.css">
<link rel="stylesheet" href="/css/sidebar.css">
<link rel="stylesheet" href="/css/breadcrumbs.css">
Questo, ovviamente, vale anche per ogni sprite map o JavaScript bundle. Trasferendo solo quello di cui si ha effettivamente bisogno, la quantità di dati trasferiti dal vostro sito può essere ridotta enormemente! Confrontate i tempi di download per bundle e file singolo mostrati con i tempi di Chrome qui sotto:

Figura 5. Download del bundle. Dopo che è stata stabilita la connessione iniziale, il bundle ci impiega 583 ms per il download su una rete 3G regolare.

Figura 6. Suddividete solo i file necessari e scaricateli in parallelo. La connessione iniziale ci mette più o meno lo stesso tempo, ma il contenuto (un foglio di stile, in questo caso) si scarica molto più rapidamente perché è più piccolo.
La prima immagine mostra che, incluso il tempo richiesto dal browser per stabilire la connessione iniziale, il bundle ha bisogno di circa 700 ms per il download su una connessione 3G regolare. La seconda immagine mostra i valori della tempistica per un file CSS su otto che compongono la pagina. L’inizio della response (TTFB) impiega altrettanto, ma dal momento che il file è molto più piccolo (meno di 1 kB), il contenuto è scaricato quasi immediatamente.
Questo potrebbe non sembrare impressionante quando si osserva un’unica risorsa, ma, come mostrato sotto, dal momento che tutti e otto i fogli di stile sono scaricati in parallelo, possiamo ancora risparmiare molto transfer time quando lo confrontiamo con l’approccio bundle.

Figura 7. Tutti i fogli di stile sul variante suddiviso si caricano in parallelo.
Si può vedere un pattern simile quando si fa passare la stessa pagina per webpagetest.org su regolare 3G. Il bundle completo (main.css
) comincia a scaricarsi appena dopo 1.5 s (linea gialla) e impiega 1.3 secondi a fare il download; il time to first meaningful paint è di circa 3.5 secondi (linea verde):

Figura 8. Download di tutta la pagina del bundle, 3G regolare.
Quando suddividiamo il bundle CSS, ogni foglio di stile comincia il download a 1.5s (linea gialla) e impiega 315-375ms per finire. Come risultato, possiamo ridurre il time to first meaningful paint di più di un secondo (linea verde):

Figura 9. Download di file singoli, 3G regolare.
Secondo le nostre misurazioni, la differenza tra i file in bundle e quelli suddivisi ha un impatto maggiore sul 3G lento rispetto al 3G normale. Su quest’ultimo, il bundle necessita di un totale di 4,5s per essere scaricato, risultando in un time to first meaningful paint attorno ai 7s:

Figura 10. Bundle, 3G lento.
La stessa pagina con file divisi su connessioni 3G lente tramite webpagetest.org produce un meaningful paint (linea verde) che si verifica 4s prima:

Figura 11. File suddivisi, 3G lento.
La cosa interessante è che ciò che è era considerato un anti-pattern delle prestazioni in HTTP/1.1 – utilizzare molti riferimenti alle risorse – diventa una best practice nell’era HTTP/2. Inoltre, la regola rimane la stessa! Il significato cambia leggermente.
La miglior richiesta è nessuna richiesta: lasciate perdere i file e il codice che non serve ai vostri utenti!
Va notato che il successo di questo approccio è fortemente legato al numero di risorse trasferite. L’esempio precedente utilizzava il 10% della libreria di fogli di stile originale, che comporta un’enorme riduzione delle dimensioni del file. Il download dell’intera libreria dell’interfaccia utente in file divisi potrebbe dare risultati diversi. Per esempio, Khan Academy ha scoperto che suddividendo i loro bundle JavaScript, la dimensione totale dell’applicazione – e pertanto il transfer time – è peggiorato drasticamente. Ciò è dovuto principalmente a due motivi: un’enorme quantità di file JavaScript (circa 100) e i poteri spesso sottostimati di Gzip.
Gzip (ma anche Brotli) produce rapporti di compressione più elevati quando c’è una ripetizione nei dati che sta comprimendo. Ciò significa che un pacchetto compresso con Gzip ha un ingombro molto inferiore rispetto ai singoli file con Gzip. Pertanto, se si intende scaricare un intero insieme di file, il rapporto di compressione delle risorse in bundle potrebbe superare quello dei singoli file scaricati in parallelo. Testate di conseguenza.
Inoltre, siate consci della vostra user base. Sebbene HTTP/2 sia stato ampiamente adottato, alcuni dei vostri utenti potrebbero essere limitati alle connessioni HTTP/1.1. Questi soffriranno per via delle risorse suddivise.
La miglior richiesta è nessuna richiesta: caching e versioning#section3
A questo punto, abbiamo visto con il nostro esempio come ottimizzare la prima visita a una pagina. Il bundle viene diviso in file separati e il client riceve solo ciò di cui ha bisogno per essere visualizzato su una pagina. Questo ci dà la possibilità di esaminare qualcosa che si tende a trascurare quando ottimizzano le prestazioni: le visite successive.
Nelle visite successive vogliamo evitare di ritrasferire degli asset inutilmente. Gli header HTTP come Cache-Control (e la loro implementazione nei server come Apache e NGINX) ci permette di memorizzare file sul disco dell’utente per un certo periodo di tempo. Alcuni server CDN hanno come default alcuni minuti. Altri alcune ore o addirittura giorni. L’idea è che durante una sessione, gli utenti non dovrebbero scaricare quello che hanno già scaricato in passato (a meno che non abbiano pulito la propria cache nel frattempo). Per esempio, la seguente header directive di Cache-Control ci garantisce che il file sia memorizzato in qualsiasi cache disponibile, per 600 secondi.
Cache-Control: public, max-age=600
Possiamo usare Cache-Control a nostro vantaggio perché sia molto più stringente. Nella nostra prima ottimizzazione abbiamo deciso di scegliere selettivamente le risorse ed essere schizzinosi riguardo a quello che trasferiamo al client, quindi memorizziamo queste risorse sulla macchina per un periodo di tempo lungo:
Cache-Control: public, max-age=31536000
Il numero di cui sopra è un anno in secondi. L’utilità di impostare a una valore alto il max-age
di Cache-Control è che l’asset sarà memorizzato dal client per un periodo di tempo lungo. Lo screenshot seguente mostra un grafico a cascata della prima visita. Ogni asset nel file HTML è richiesto:

Figura 12. Prima visita: ogni asset è richiesto.
Con gli header di Cache-Control impostati appropriatamente, una visita seguente risulterà in meno richieste. Lo screenshot sottostante mostra che tutti gli asset richiesti sul nostro dominio di test non fanno scattare alcuna richiesta. Gli asset da un altro dominio con gli header di Cache-Control impostate in maniera impropria fanno ancora scattare una singola richiesta, così come fanno le risorse che non sono state trovate:

Figura 13. Seconda visita: vengono richiesti di nuovo solo alcuni SVG messi malamente in cache da un server differente.
Quando si tratta di invalidare un asset in cache (il che, di conseguenza, è una delle due cose più difficili in informatica), usiamo semplicemente un nuovo asset. Vediamo come funzionerebbe con il nostro esempio. Il caching funziona basandosi sui nomi di file. Un nuovo nome di file può far scattare un nuovo download. Precedentemente, abbiamo suddiviso la nostra code base in pezzi ragionevoli. Un version indicator ci assicura che ogni nome di file rimane unico:
<link rel="stylesheet" href="/css/header.v1.css">
<link rel="stylesheet" href="/css/article.v1.css">
Dopo una modifica ai nostri stili dell’articolo, dovremmo modificare il numero di versione:
<link rel="stylesheet" href="/css/header.v1.css">
<link rel="stylesheet" href="/css/article.v2.css">
Un’alternativa al tenere traccia della versione del file è di impostare un revision hash basato sul contenuto del file con dei tool di automazione.
Va bene memorizzare i propri asset sul client per un periodo di tempo. Tuttavia, il vostro HTML dovrebbe essere più transitorio nella maggior parte dei casi. Tipicamente, il file HTML contiene le informazioni su quali risorse scaricare. Se voleste che le vostre risorse cambino (come caricare article.v2.css invece di article.v1.css, come abbiamo appena visto), dovrete aggiornare i riferimenti ad essi nel vostro HTML. I server CDN popolari mettono in cache HTML per non più di sei minuti, ma potete decidere cosa va meglio per la vostra applicazione.
Di nuovo, la miglior richiesta è nessuna richiesta: memorizzate i file sul client per quanto più tempo possibile e non richiedeteli di nuovo. Le edizioni recenti di Firefox e Edge hanno addirittura una direttiva immutabile per Cache-Control, che prende di mira proprio questo pattern.
Conclusioni#section4
HTTP/2 è stato progettato da zero per gestire le inefficienze di HTTP/1. Innescare un gran numero di richieste in un ambiente HTTP/2 non è intrinsecamente più negativo per la performance, ma trasferire dati non necessari lo è.
Per raggiungere il pieno potenziale di HTTP/2, dobbiamo osservare ogni caso individualmente. Un’ottimizzazione che potrebbe andar bene per un sito web potrebbe avere un effetto negativo su un altro. Con tutti i benefici che derivano da HTTP/2, la regola d’oro dell’ottimizzazione della performance si applica ancora: la miglior richiesta è nessuna richiesta. Solo che questa volta diamo un’occhiata alla quantità effettiva di dati trasferiti.
Trasferite solo quello che effettivamente serve ai vostri utenti. Niente di più, niente di meno.
Nessun commento
Altro da ALA
Webwaste
Uno strumento essenziale per catturare i vostri progressi lavorativi
Andiamo al cuore dell’accessibilità digitale
JavaScript Responsabile, Parte II
JavaScript Responsabile: parte prima