Ogni sito veloce che visiti è veloce grazie alla cache. Non per via di codice migliore, database più rapidi o server più costosi. Cache. La stessa query che la prima volta impiega 200 millisecondi la seconda volta ne impiega 0,1, perché qualcuno ha salvato il risultato. Moltiplica la cosa per centinaia di query a pagina e tutto il web inizia ad avere senso.
Quasi tutti i problemi di performance sono problemi di caching. O manca una cache dove servirebbe, o è "calda" quando non dovrebbe (l'utente ha aggiornato il contenuto e continua a vedere la versione vecchia). Per fare debug in entrambi i casi devi sapere quali cache esistono, dove stanno e cosa gestisce ognuna.
Fra il tuo database e gli occhi del visitatore ci sono cinque livelli principali. Li percorriamo dall'interno verso l'esterno.
1. PHP OPcache (o l'equivalente per altri linguaggi)
Vive dentro l'interprete PHP sul tuo web server. Memorizza il bytecode PHP compilato, non i dati. Ogni volta che PHP legge un file .php deve trasformarlo in una rappresentazione interna prima di eseguirlo. OPcache tiene quella forma compilata in memoria condivisa, così PHP salta il parsing alla richiesta successiva.
Non lo configuri richiesta per richiesta. Lo abiliti una volta in php.ini, gli dai 128-256 MB di memoria, e accelera ogni applicazione PHP sul server di un fattore 2-5. WordPress senza OPcache va circa alla metà della sua velocità reale.
Lo stesso concetto esiste in altri stack: __pycache__ di Python, l'inline caching di V8 in Node, il JIT di Java. Meccanismi diversi, stesso lavoro: non ricompilare lo stesso codice ad ogni richiesta.
Pensi a OPcache solo quando qualcosa va storto: deploy in cui il codice nuovo non viene preso (ti sei dimenticato di resettare OPcache), pressione sulla memoria (la cache si riempie e inizia a buttare via roba). Su un server sano lo configuri una volta e te lo dimentichi.
2. Object cache (Redis o Memcached)
Vive sul server, in un processo separato da PHP. Memorizza dati arbitrari, di solito risultati di query al database, indicizzati per chiave. WordPress, Drupal, Magento, Laravel, tutti supportano un object cache via Redis. Lo schema è questo:
$cache_key = 'user_posts_' . $user_id;
$result = $redis->get($cache_key);
if ($result === null) {
$result = $db->query('SELECT ...');
$redis->set($cache_key, $result, 3600);
}
La prima richiesta esegue la query, le successive 3.600 secondi di richieste la saltano. Su una homepage tipica di WordPress con questo schema risparmi decine di query MySQL.
Redis è il default moderno. Memcached funziona ancora bene ma Redis ha più strutture dati e persistenza. Si installano in 5 minuti. Il plugin che metti sopra a WordPress (Redis Object Cache, W3 Total Cache, LiteSpeed Cache, Object Cache Pro) traduce le chiamate dell'applicazione in chiamate Redis.
La cosa da sapere: l'object cache non riduce il numero di processi PHP che gestiscono una richiesta. PHP si sveglia comunque, esegue il tuo codice, va su Redis invece che su MySQL. È più veloce, ma PHP-FPM lavora lo stesso. Per saltare PHP del tutto serve il livello successivo.
3. Page cache (HTML completo della pagina salvato come file o in memoria)
È il livello che trasforma una richiesta WordPress da 1.500 millisecondi in una risposta da file statico da 50 millisecondi. Una volta che una pagina è stata generata, salvi l'HTML renderizzato completo da qualche parte di veloce. Il visitatore successivo che chiede lo stesso URL riceve l'HTML salvato direttamente, senza PHP, senza database, senza niente.
Due modi di implementarlo su un Linux tipico:
Su file: WP Rocket, WP Super Cache, LiteSpeed Cache. L'HTML generato finisce in wp-content/cache/. Il web server lo serve come se fosse un file statico.
A livello server: nginx FastCGI cache o nginx + Redis (il modulo srcache_nginx che WordOps usa di default). L'HTML sta nella memoria di nginx o in Redis, e nginx lo serve senza chiamare PHP.
La versione su file è più semplice e funziona anche su hosting condiviso. La versione a livello server è più veloce, ha un footprint di memoria minore per richiesta, e sopravvive a PHP rotto (la home si carica anche se PHP-FPM è in crash).
La parte difficile è l'invalidazione. Quando pubblichi un post, la page cache della homepage, della categoria, dell'archivio, dei tag, del feed RSS, devono tutte scadere. I plugin gestiscono la maggior parte dei casi. Non sempre li gestiscono tutti. Il bug classico è "ho cambiato il prezzo e per 24 ore i clienti vedono quello vecchio" perché la page cache non è stata invalidata quando il campo prezzo è stato aggiornato.
Se il sito è dinamico (utenti loggati, carrello, contenuti personalizzati), non puoi cachare la pagina intera. O cachi a frammenti, o cachi solo per i visitatori anonimi e lasci PHP a quelli loggati.
4. CDN (content delivery network)
Una CDN è una rete di server distribuiti nel mondo che si frappongono fra il tuo origin server e i visitatori. Il visitatore di Berlino si collega a un nodo CDN a Francoforte invece di raggiungere il tuo server Hetzner di Helsinki. La CDN o ha già il contenuto in cache e lo serve in locale, o va a chiederlo al tuo server e tiene la risposta per la volta dopo.
Cosa accelera davvero una CDN:
- Asset statici (immagini, CSS, JS, font, video). Sempre. Per default. Questo è l'80% facile del beneficio.
- Pagine HTML, ma solo se lo configuri. Il default di Cloudflare per l'HTML è
cf-cache-status: DYNAMIC, ovvero "non cachiamo l'HTML se non ci dici tu di farlo". Lo abiliti con una Cache Rule (o le vecchie Page Rules) sul dominio.
Senza HTML caching alla CDN, ogni richiesta di pagina arriva comunque sul tuo origin. Con HTML caching alla CDN, la richiesta nemmeno raggiunge il tuo server per le pagine in cache, e il carico sull'origin si divide per 10 o 20 su un sito con traffico vero.
La complicazione è la stessa della page cache: invalidazione. Quando pubblichi devi fare il purge della CDN. Cloudflare e Fastly hanno API e plugin. BunnyCDN ha il purge per URL. Alcune CDN richiedono di aspettare la scadenza del TTL, cosa poco pratica se il TTL è 24 ore.
Una seconda cosa che la CDN ti dà: la terminazione TLS al bordo. L'origin può servire HTTP semplice alla CDN su connessione privata, e la CDN gestisce HTTPS verso il visitatore. Risparmi CPU sull'origin e puoi spostare verso l'edge security headers, redirect, rate limiting.
5. Browser cache
Il browser del visitatore tiene i file in locale. Il browser chiede al tuo server (o alla CDN) "hai una versione più nuova di style.css rispetto a quella che ho in cache da martedì?", e il server risponde "no, usa la tua" (304 Not Modified, quasi gratis) oppure "sì, eccola" (200 OK con il file nuovo).
Controlli la browser cache con gli header HTTP:
Cache-Control: public, max-age=31536000, immutableper asset con un hash nel nome (style.abc123.css). Cache per un anno, mai ricontrollare.Cache-Control: public, max-age=3600, must-revalidateper HTML e asset senza versioning. Cache per un'ora, poi ricontrolla.Cache-Control: no-storeper dati personali o sensibili. Mai cachare.- Gli header
ETageLast-Modifiedpermettono al browser le richieste condizionali.
La maggior parte dei guadagni di performance sugli asset statici nei progetti reali viene da questo livello. Un visitatore di ritorno con tutto in cache locale carica il sito in 200 millisecondi perché il browser scarica solo l'HTML, non gli 800 KB di CSS, JS, font, immagini. Un visitatore nuovo paga il costo pieno.
Come si impilano
In ordine, dalla richiesta del visitatore al tuo database:
- Il browser controlla la cache locale. Hit, fine.
- Il browser chiede alla CDN. Hit, fine.
- La CDN chiede alla page cache del tuo origin. Hit, restituito senza svegliare PHP.
- PHP si sveglia, esegue il tuo codice, chiede a Redis i risultati delle query in cache.
- Redis miss, MySQL viene interrogato, risultato salvato in Redis, codice eseguito, HTML restituito.
- HTML salvato nella page cache (3), inoltrato alla CDN (2) che lo memorizza, inoltrato al browser (1) che lo memorizza.
La prima richiesta tocca tutti e cinque i livelli ed è lenta. La seconda richiesta identica dallo stesso browser arriva dal livello 1 ed è istantanea. La richiesta di un altro visitatore in un'altra città arriva dal livello 2 ed è veloce. Più la cache è vicina al visitatore, più la richiesta è rapida.
Cosa ti dà tutto questo
Quando una cosa è lenta, non ti chiedi "è lento il database?". Ti chiedi "quale cache sta saltando?". La risposta ti dice cosa sistemare:
- Il TTFB è di 1,5 secondi e non scende mai: la page cache manca o salta sempre (spesso un cookie o una query string esclude la pagina dalla cache). Indaga
srcache_fetch_status,cf-cache-status, o il log di debug del tuo plugin. - Il TTFB scende al secondo hit ma poi risale: la cache viene espulsa, di solito perché Redis o la zona di nginx cache è sottodimensionata.
- Veloce sulle pagine nuove, lento su quelle modificate: la cache esiste ma non viene invalidata quando il contenuto cambia.
- Veloce nel tuo paese, lento dagli altri: la CDN non è attiva sull'HTML, o l'origin non è dietro CDN per quella route.
Le cinque cache qui sopra coprono il 95% delle domande di performance che incontri su un sito PHP tipico. Il restante 5% è inefficienza a livello applicativo (query lente, N+1, script di terze parti), che è un'altra conversazione.