Indice dei contenuti dell'articolo:
Quando si parla di velocità di un sito web, uno dei primi argomenti che emergono è quasi sempre la Web Cache. Il motivo è semplice: evitare di rigenerare da zero una pagina a ogni richiesta consente di ridurre drasticamente il lavoro necessario lato server e, di conseguenza, di restituire il contenuto all’utente molto più rapidamente.
Il principio alla base è relativamente intuitivo. Un CMS dinamico come WordPress, quando deve generare una pagina, può essere costretto a eseguire codice PHP, interrogare il database, caricare configurazioni, elaborare plugin, recuperare informazioni e assemblare infine il documento HTML da inviare al browser. Se la pagina risultante viene invece conservata in cache, molte delle richieste successive possono evitare buona parte di questa elaborazione e ricevere direttamente una copia già pronta.
Negli anni questo concetto è diventato accessibile anche a un pubblico non necessariamente tecnico. Oggi esistono server e stack che integrano sistemi di caching, soluzioni come LiteSpeed, plugin WordPress come WP Rocket, FlyingPress e numerosi altri strumenti che permettono di ottenere risultati interessanti senza dover necessariamente conoscere nel dettaglio il funzionamento di un reverse proxy.
All’altro estremo troviamo tecnologie come Varnish, (la nostra scelta preferita) storicamente molto utilizzate in ambienti enterprise e in infrastrutture dove è necessario avere un controllo estremamente preciso delle politiche di caching, dell’invalidazione dei contenuti e del comportamento delle richieste.
Tutto molto bello. Ma c’è un problema.
Avere una Web Cache configurata non significa automaticamente avere un sito costantemente veloce.
Ed è proprio qui che bisogna smettere di guardare soltanto il tempo di caricamento di una singola URL e cominciare a ragionare su un parametro molto più significativo: quante richieste riescono realmente a trovare il contenuto in cache durante la normale navigazione del sito?
Una pagina velocissima non significa avere un sito veloce
Uno degli errori più comuni nella valutazione delle performance è prendere una pagina, aprirla alcune volte, verificarla con uno strumento di test e concludere che il sistema di cache funzioni correttamente.
Magari la homepage risponde molto velocemente. Una pagina prodotto altrettanto. La pagina di una categoria sembra perfetta.
Poi si comincia a navigare davvero.
Si apre un altro prodotto e la risposta impiega sensibilmente più tempo. Si passa alla categoria successiva e torna veloce. Si apre un articolo e rallenta di nuovo. Si torna indietro, si entra in un’altra sezione e improvvisamente tutto sembra nuovamente rapido.
Il risultato è quella che potremmo definire una navigazione a singhiozzo: alcune pagine sembrano istantanee, altre si comportano come se la cache non esistesse.
Dal punto di vista tecnico è perfettamente possibile che sia proprio così.
Quando una richiesta trova una copia della pagina disponibile in cache abbiamo normalmente un cache HIT. Il server può fornire il contenuto senza dover ricostruire completamente la pagina attraverso l’applicazione.
Quando invece quella copia non è disponibile abbiamo un MISS. La richiesta deve quindi raggiungere i livelli sottostanti dello stack e, a seconda dell’architettura, può comportare elaborazione PHP, query SQL, chiamate verso servizi esterni e tutto ciò che normalmente serve per generare quella determinata pagina.
Ed ecco perché il dato realmente interessante non è semplicemente sapere se la cache è attiva.
È importante conoscere il suo HIT Ratio, cioè la proporzione delle richieste che possono essere soddisfatte direttamente dalla cache rispetto a quelle che devono attraversare nuovamente il backend.
Una cache velocissima con un HIT Ratio basso può produrre un’esperienza utente peggiore di una cache correttamente dimensionata e progettata per mantenere un HIT Ratio elevato e costante.
Il benchmark sulla singola URL può raccontare solo metà della storia o anche meno
Il problema è particolarmente evidente quando si utilizzano benchmark sintetici.
Supponiamo di testare dieci volte la stessa pagina. Alla prima richiesta il sistema genera il contenuto e lo inserisce in cache. Le nove richieste successive trovano la pagina già disponibile.
Il risultato può sembrare eccellente.
Ma un utente reale non visita dieci volte consecutivamente la stessa URL.
Un utente entra nella homepage, apre una categoria, visita un prodotto, torna alla categoria, applica un filtro, apre un secondo prodotto, legge una pagina informativa e continua la navigazione.
Su un sito editoriale può passare dalla homepage a un articolo, poi a un approfondimento correlato, quindi a una categoria o a un contenuto pubblicato mesi prima.
È in questo scenario che emerge la vera qualità della strategia di caching.
Se soltanto le pagine più visitate sono presenti in cache mentre gran parte della cosiddetta long tail delle URL richiede continuamente una nuova generazione, l’esperienza reale può essere molto diversa da quella mostrata dal benchmark effettuato sulla homepage.
Il punto non è quindi ottenere uno screenshot con un tempo di risposta eccezionale.
Il punto è fare in modo che la maggior parte della navigazione reale mantenga un comportamento coerente e prevedibile.
Il problema dei purge completi della cache
Uno dei motivi per cui l’HIT Ratio può crollare improvvisamente è il modo in cui alcuni sistemi gestiscono l’invalidazione della cache.
In determinate configurazioni, un evento relativamente comune può provocare la cancellazione di una porzione molto ampia della cache o addirittura del suo intero contenuto.
Può accadere, ad esempio, in seguito all’aggiornamento di un plugin, alla modifica di alcune impostazioni, alla pubblicazione di contenuti, a interventi amministrativi o semplicemente quando l’utente preme il classico pulsante “Pulisci tutta la cache”.
Da quel momento il sito è formalmente ancora dotato di un sistema di caching, ma la cache è vuota.
Ogni URL richiesta per la prima volta diventa quindi un MISS.
La homepage viene rigenerata. Il primo prodotto viene rigenerato. La prima categoria viene rigenerata. Il secondo prodotto viene rigenerato. Un vecchio articolo viene rigenerato.
Man mano che gli utenti navigano, la cache torna gradualmente a riempirsi. Nel frattempo, però, sono gli stessi visitatori a pagare il costo della sua ricostruzione.
È una situazione particolarmente insidiosa perché, dopo qualche minuto o qualche ora, le pagine più popolari saranno nuovamente disponibili in cache e un controllo superficiale potrebbe far pensare che sia tutto perfettamente funzionante.
Il problema rimane sulle URL meno frequentate e si ripresenta nuovamente al purge successivo.
Cache warm-up: perché si cerca di riempire la cache prima degli utenti
Una delle tecniche utilizzate per limitare questo comportamento è il cosiddetto cache warm-up.
Il concetto è semplice: invece di aspettare che sia il primo visitatore a richiedere una pagina non ancora presente in cache, un processo automatico visita preventivamente le URL del sito, facendo generare le relative copie.
In teoria è una soluzione elegante.
Si svuota la cache, parte il processo di warm-up e le pagine vengono progressivamente ricostruite prima che arrivino gli utenti.
Il problema emerge quando si passa da un piccolo sito con poche centinaia di URL a un portale, un magazine o un ecommerce con decine o centinaia di migliaia di pagine, se non addirittura milioni.
Immaginiamo un sito con un milione di URL potenzialmente memorizzabili. Per eseguire un warm-up completo bisogna effettuare un numero enorme di richieste e, soprattutto, ogni pagina che non è ancora disponibile deve essere generata realmente dal backend.
Questo significa consumo di CPU, utilizzo del database, I/O, memoria, processi PHP e tempo.
In pratica, un warm-up eseguito senza criterio rischia paradossalmente di comportarsi come un piccolo stress test lanciato contro il proprio sito.
E c’è un altro problema: mentre il crawler incaricato del warm-up sta ricostruendo le pagine, gli utenti continuano a navigare.
Se il processo impiega molto tempo per coprire l’intero sito, una parte consistente delle URL continuerà comunque a essere scoperta prima da visitatori reali.
Una cache deve essere dimensionata per il sito che deve servire
È qui che entra in gioco l’esperienza sistemistica.
Installare un sistema di cache è relativamente semplice. Progettare una cache adeguata al volume, alla tipologia di contenuti e al traffico di un sito è un’altra cosa.
Una cache che deve gestire poche migliaia di pagine ha esigenze completamente diverse da quella di un ecommerce con centinaia di migliaia di prodotti, filtri, categorie e varianti.
Bisogna considerare quanto spazio riservare agli oggetti, con quali criteri conservarli, quanto a lungo mantenerli disponibili, quali richieste devono essere escluse e soprattutto come evitare che oggetti poco utili espellano continuamente dalla cache quelli realmente importanti.
Un errore che incontriamo, ad esempio, è utilizzare risorse preziose della cache applicativa o della cache delle pagine per conservare file statici molto voluminosi come immagini e video.
Questo non significa che immagini, CSS, JavaScript o altri asset non debbano essere memorizzati da qualche parte. Al contrario, per queste risorse esistono meccanismi specifici molto efficienti: cache del browser, cache del web server, CDN e object storage sono esempi di livelli che possono occuparsene.
Il punto è evitare di riempire indiscriminatamente uno spazio progettato per conservare oggetti preziosi con file che potrebbero essere gestiti in modo più appropriato altrove.
Su siti molto grandi, la capacità effettiva della cache e la politica con cui vengono conservati o rimossi gli oggetti diventano parte integrante delle performance.
Il vero problema della navigazione a singhiozzo negli ecommerce
Su un blog una pagina occasionalmente più lenta può essere fastidiosa.
Su un ecommerce può diventare molto più grave.
Quando un utente sta confrontando prodotti, colori, modelli, caratteristiche o prezzi, tende a effettuare molte navigazioni consecutive. La velocità percepita non dipende quindi dal caricamento di una singola landing page, ma dalla continuità dell’intera sessione.
Una categoria che risponde immediatamente, seguita da un prodotto lento, poi da un altro prodotto veloce e successivamente da una nuova attesa crea una sensazione di instabilità.
L’utente non sa che dietro le quinte alcune richieste sono HIT e altre MISS.
Percepisce semplicemente che “il sito a volte è veloce e a volte no”.
Ed è una delle esperienze più frustranti perché rende imprevedibile ogni clic.
In un percorso di acquisto questa frizione può sommarsi ad altre piccole incertezze e contribuire all’abbandono della navigazione o del processo di conversione. Un ecommerce dovrebbe invece cercare di offrire una risposta il più possibile uniforme durante l’esplorazione del catalogo.
TTFB, LCP e Core Web Vitals: perché la continuità conta
La disponibilità della pagina in cache ha un effetto particolarmente evidente sul Time To First Byte, o TTFB, cioè sul tempo che trascorre prima che il browser inizi a ricevere la risposta richiesta.
È importante precisare che il TTFB non è di per sé un Core Web Vital. Tuttavia arriva all’inizio della catena di caricamento della pagina: se il documento HTML tarda a essere restituito, il browser inizierà più tardi anche a scoprire e processare molte delle risorse necessarie alla visualizzazione.
Di conseguenza, un TTFB elevato può ripercuotersi sulle metriche che vengono dopo, tra cui il Largest Contentful Paint (LCP).
Ed è ancora una volta il comportamento discontinuo a essere interessante.
Non basta avere un TTFB eccellente sulla homepage già memorizzata se una porzione significativa delle navigazioni reali deve continuamente raggiungere un backend molto più lento.
Per questo motivo, quando analizziamo un’infrastruttura, non ci interessa soltanto conoscere quanto è veloce il miglior HIT possibile. Ci interessa capire quanto spesso quell’HIT avviene realmente.
Il nostro approccio: invalidare il contenuto senza scaricare il problema sul primo visitatore
In Managed Server abbiamo sviluppato un approccio diverso alla gestione di questi scenari.
L’obiettivo è evitare che una normale operazione di aggiornamento o di pulizia trasformi improvvisamente migliaia di URL veloci in altrettante richieste lente.
La logica consiste nel separare il momento in cui un contenuto viene considerato da aggiornare dal momento in cui la sua ultima copia utilizzabile viene effettivamente abbandonata.
Quando una pagina deve essere rigenerata, il sistema può continuare a disporre di una rappresentazione precedentemente memorizzata attraverso un livello intermedio. Nel frattempo viene prodotta una nuova versione che andrà a sostituire quella precedente.
In altre parole, invece di avere questa sequenza:
- cancellazione della pagina dalla cache;
- arrivo del primo utente;
- attesa della generazione PHP e delle query al database;
- creazione della nuova cache;
- risposta finalmente disponibile;
possiamo fare in modo che l’utente continui a ricevere una copia immediatamente servibile mentre il contenuto viene aggiornato dietro le quinte.
Il risultato pratico è importante perché il processo di refresh non viene più trasformato automaticamente in un rallentamento percepibile dal visitatore.
Questo principio può essere applicato anche quando viene richiesta una pulizia molto estesa della cache: l’architettura evita, per quanto possibile, che il purge logico coincida con la distruzione immediata dell’ultima risposta pronta da fornire.
Il vantaggio non è avere il record di velocità, ma eliminare i picchi di lentezza
Questo introduce un cambio di prospettiva.
Nel mondo delle performance si tende spesso a cercare il numero più basso possibile: 100 millisecondi, 80 millisecondi, 50 millisecondi.
Ma per un utente può essere molto più importante avere venti pagine consecutive che rispondono in maniera uniforme rispetto ad avere dieci pagine istantanee alternate ad altre dieci che richiedono attese molto più lunghe.
La performance reale è anche prevedibilità.
Un’architettura di caching efficace deve cercare non soltanto di abbassare la media, ma anche di ridurre la distanza tra il comportamento migliore e quello peggiore.
Questo significa mantenere più stabile il TTFB durante la navigazione, ridurre il numero di richieste che ricadono improvvisamente sull’application server e limitare i picchi di carico che si verificano dopo invalidazioni massive.
Il beneficio è quindi doppio.
Da una parte l’utente trova un sito più coerente e reattivo. Dall’altra il backend viene protetto da quelle ondate di rigenerazioni simultanee che possono verificarsi proprio nel momento peggiore, ad esempio dopo una modifica alla cache mentre il sito sta ricevendo molto traffico.
Un HIT Ratio elevato protegge anche PHP e database
La Web Cache non serve soltanto per mostrare velocemente una pagina.
Serve anche a evitare lavoro inutile all’infrastruttura sottostante.
Ogni richiesta che può essere soddisfatta a un livello di caching adeguato è potenzialmente una richiesta che non deve arrivare a WordPress, PHP o al database.
Quando l’HIT Ratio diminuisce, invece, il backend deve assorbire una quantità maggiore di traffico.
Questo fenomeno può creare un circolo vizioso.
La cache viene svuotata. Aumentano i MISS. Aumenta il numero di elaborazioni PHP. Crescono le query al database. Il server impiega più risorse. Le pagine dinamiche diventano ancora più lente proprio mentre il sistema sta cercando di ricostruire la cache.
Su siti ad alto traffico il problema può essere particolarmente evidente perché più utenti possono richiedere contemporaneamente oggetti non ancora ricostruiti.
Ecco perché una strategia professionale deve preoccuparsi non soltanto di come creare la cache, ma anche di come gestirne il ciclo di vita, il refresh, l’invalidazione e la ricostruzione.
Anche i crawler beneficiano di un’infrastruttura che risponde in modo costante
La continuità delle performance riguarda anche i crawler dei motori di ricerca.
Un sito con un numero elevato di URL può ricevere continuamente richieste da Googlebot e da altri crawler. Se una buona parte di queste richieste può essere servita rapidamente senza coinvolgere pesantemente il backend, l’infrastruttura riesce a sostenere più facilmente l’attività di scansione.
Google stesso tiene conto della capacità del server di rispondere alle richieste: quando un’infrastruttura rallenta significativamente o manifesta errori, i crawler possono ridurre la pressione per evitare di sovraccaricarla.
Questo non significa che una cache migliore garantisca automaticamente un migliore posizionamento, né che aumentare la velocità di scansione rappresenti di per sé un fattore di ranking.
Significa però che un server rapido, stabile e capace di gestire il crawling senza saturarsi crea condizioni tecniche migliori per una scansione efficiente, soprattutto su siti molto grandi.
Non chiederti soltanto “ho la cache?”, chiediti “quanto spesso la sto usando?”
La Web Cache rimane uno degli strumenti più importanti per migliorare le prestazioni di un sito dinamico.
Ma la sola presenza di LiteSpeed, Varnish, di un plugin WordPress o di qualsiasi altro sistema non è sufficiente per stabilire se la strategia stia realmente funzionando.
Bisogna osservare il comportamento dell’intero sito.
Bisogna capire cosa succede dopo un purge.
Bisogna verificare quanto tempo serve per ricostruire le URL.
Bisogna valutare se la cache ha spazio sufficiente per il volume reale di contenuti e se le sue politiche di conservazione sono compatibili con il progetto.
Soprattutto, bisogna osservare l’HIT Ratio nel traffico reale.
Perché il risultato che conta non è mostrare che una pagina può essere estremamente veloce quando tutte le condizioni sono favorevoli.
Il risultato che conta è fare in modo che quella velocità sia disponibile sulla maggior parte delle pagine e per la maggior parte del tempo.
La Web Cache è bella. L’HIT Ratio è meglio.
Una Web Cache ben configurata può trasformare radicalmente le performance di WordPress, WooCommerce e di qualsiasi applicazione dinamica. Tuttavia, quando il sito cresce, il problema non è più semplicemente attivare una cache.
Il problema diventa mantenerla efficace nel tempo.
Un sistema che viene completamente svuotato a ogni modifica e ricostruito progressivamente sulle spalle degli utenti può ottenere benchmark spettacolari sulle URL già riscaldate e contemporaneamente offrire una pessima esperienza sul resto del sito.
Per questo in Managed Server preferiamo ragionare in termini di continuità, percentuale di HIT e protezione del backend.
Attraverso un’architettura che conserva una copia intermedia utilizzabile mentre i contenuti vengono rigenerati, possiamo evitare che le normali operazioni di refresh della cache producano improvvisamente una navigazione a singhiozzo.
Questo significa TTFB più uniforme, meno richieste costose verso PHP e database, minori picchi di carico, migliore esperienza di navigazione e condizioni più favorevoli anche per le metriche di caricamento e per l’attività dei crawler.
Perché una cache non dovrebbe essere giudicata soltanto da quanto è veloce quando viene colpita.
Dovrebbe essere giudicata soprattutto da quanto spesso riesce a farsi trovare pronta.