Questo articolo è stato pubblicato per la prima volta nel 2010 e completamente riscritto a ottobre 2026: i commenti più vecchi si riferiscono alla versione originale.
Alla fine del 2010 scrivevo di qualità del codice e di cache «manuali», mostrando con un certo orgoglio come salvare in memoria l’elenco degli ultimi cinque articoli con wp_cache_set(), come passare alle allora nuovissime Transients API e come mettere su disco interi pezzi di template con una funzione scritta da me. Gli esempi di codice di quel post li ho poi sistemati a mano più volte, segno che anche la cache, come i buoni propositi, va rinfrescata ogni tanto.
Nel 2026 la cache in WordPress è diventata una faccenda a strati: c’è quella del browser, quella della CDN, la page cache sul server, l’object cache persistente con Redis o Memcached e, sotto a tutto, i transient. Ogni livello risolve un problema diverso e, soprattutto, si invalida in modo diverso, ed è proprio lì che nascono le giornate passate a chiedersi perché la modifica appena salvata non si vede. In questo articolo li mettiamo in fila, dal browser al database, con un esempio di codice aggiornato e corretto.
I livelli di cache, dal browser al database
Una cache è una copia di un risultato già calcolato, conservata più vicino a chi la chiede, per non doverlo ricalcolare a ogni richiesta. In WordPress una pagina viene costruita da PHP interrogando il database decine di volte, quindi ogni livello di cache che intercetta la richiesta prima di arrivare a quel punto fa risparmiare tempo, CPU e query.
Partendo dal visitatore e scendendo verso il database, i livelli che incontri in un sito WordPress moderno sono questi:
- Cache del browser: conserva sul dispositivo dell’utente immagini, CSS e JavaScript, guidata dagli header HTTP come
Cache-Control. - CDN: una rete di server distribuiti che conserva copie dei file statici e, se configurata, anche dell’HTML, vicino a chi visita il sito.
- Page cache: sul server salva l’HTML completo delle pagine, così per i visitatori non autenticati WordPress e PHP non vengono nemmeno avviati.
- Object cache: conserva in memoria i risultati di query e calcoli interni di WordPress; di base dura una sola richiesta, con Redis o Memcached diventa persistente.
- Transients: valori con scadenza salvati dal codice, nel database o nell’object cache persistente se presente.

La regola pratica è semplice: più il livello è vicino all’utente, più è veloce e più è difficile da invalidare in modo selettivo. Per questo i contenuti che cambiano spesso vivono bene negli strati bassi, mentre i file che non cambiano mai, come un CSS versionato, possono restare a lungo nel browser e nella CDN.
Transients API: cosa sono e come usarle oggi
Le Transients API servono a salvare il risultato di un’operazione costosa con una scadenza, per riusarlo nelle richieste successive e tra utenti diversi. Le funzioni sono sempre le stesse del 2010, set_transient(), get_transient() e delete_transient(), ma ci sono due dettagli che la documentazione ufficiale sottolinea e che vedo ignorati spesso.
Il primo è che la scadenza è un tempo massimo, non minimo: un transient può sparire prima, per esempio perché l’object cache ha liberato memoria, quindi il codice deve sempre prevedere il caso in cui il valore non c’è. Il secondo è che, senza object cache persistente, i transient finiscono nella tabella wp_options, mentre con Redis o Memcached vivono in memoria e il database non viene toccato affatto.
L’esempio del 2010, rifatto come si deve
Questo è lo stesso esempio di allora, l’elenco degli ultimi cinque articoli, riscritto con WP_Query invece di una query SQL a mano, con l’escaping dell’output e con un’invalidazione che scatta quando un articolo viene pubblicato, modificato o tolto dalla pubblicazione:
<?php
/**
* Restituisce l'HTML degli ultimi 5 articoli, salvato in un transient per un'ora.
*/
function mavida_ultimi_post_html() {
// 1. Se il transient esiste lo restituisco subito.
$html = get_transient( 'mavida_ultimi_post_html' );
if ( false !== $html ) {
return $html;
}
// 2. Altrimenti eseguo una query leggera: niente paginazione né meta.
$query = new WP_Query( array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 5,
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
) );
// 3. Costruisco l'HTML con l'escaping di URL e titoli.
$html = '<ul>';
foreach ( $query->posts as $post ) {
$html .= sprintf(
'<li><a href="%s">%s</a></li>',
esc_url( get_permalink( $post ) ),
esc_html( get_the_title( $post ) )
);
}
$html .= '</ul>';
// 4. Salvo il risultato: la scadenza è un massimo, non una garanzia.
set_transient( 'mavida_ultimi_post_html', $html, HOUR_IN_SECONDS );
return $html;
}
/**
* Invalida la cache quando un articolo entra, resta o esce dallo stato "publish".
*/
add_action( 'transition_post_status', function ( $new_status, $old_status, $post ) {
if ( 'post' === $post->post_type && ( 'publish' === $new_status || 'publish' === $old_status ) ) {
delete_transient( 'mavida_ultimi_post_html' );
}
}, 10, 3 );Rispetto alla versione originale cambiano tre cose importanti: il confronto con false è stretto, quindi una stringa vuota non viene scambiata per un dato mancante; l’HTML è costruito in modo sicuro; e l’hook transition_post_status copre anche il caso, che nel 2010 mi ero perso, di un articolo che viene rimesso in bozza o cestinato.
Object cache persistente con Redis o Memcached
L’object cache di WordPress è il sistema interno, basato sulle funzioni wp_cache_*, in cui il core conserva opzioni, articoli, termini e metadati già letti dal database. Di base non è persistente: i dati restano in memoria solo per la durata della singola richiesta, esattamente come scrivevo nel 2010, e alla richiesta successiva si riparte da zero.
Per renderla persistente serve un drop-in object-cache.php collegato a un server di cache, che oggi nella maggior parte dei casi è Redis o Memcached; il plugin Redis Object Cache è la strada più battuta, e anche LiteSpeed Cache include il supporto a Redis e Memcached tra le sue funzioni. La differenza si sente soprattutto dove la page cache non può aiutare, cioè nell’amministrazione, per gli utenti autenticati, nei carrelli di WooCommerce e nelle chiamate al REST API; non a caso è proprio il genere di problema strutturale, il caricamento delle opzioni a ogni richiesta, che citavo parlando di WordPress 7.0.
Invalidare senza cancellare: last_changed
Con l’object cache persistente c’è un modo elegante di invalidare che il core usa da anni: invece di cancellare le chiavi, si inserisce nel nome della chiave il valore restituito da wp_cache_get_last_changed(), che WordPress aggiorna da solo ogni volta che un articolo viene salvato. Quando il contenuto cambia, cambia la chiave, e la vecchia voce semplicemente non viene più letta:
<?php
// La chiave cambia da sola quando cambia un qualsiasi articolo (gruppo "posts").
$key = 'ultimi_post_ids:' . wp_cache_get_last_changed( 'posts' );
$ids = wp_cache_get( $key, 'mavida' );
if ( false === $ids ) {
$ids = get_posts( array(
'numberposts' => 5,
'fields' => 'ids',
) );
wp_cache_set( $key, $ids, 'mavida', DAY_IN_SECONDS );
}Ricorda però che senza un backend persistente questo codice funziona lo stesso, ma la cache dura una sola richiesta: il vantaggio vero arriva solo con Redis o Memcached.
Page cache: il caso LiteSpeed Cache
La page cache salva l’HTML finito di una pagina e lo restituisce ai visitatori successivi senza avviare WordPress: è il livello che incide di più sul tempo di risposta del server, perché salta PHP e database in un colpo solo. Questo sito, per esempio, gira su un server LiteSpeed con il plugin LiteSpeed Cache, e se guardi gli header della home vedi x-litespeed-cache-control: public,max-age=604800, cioè una page cache valida fino a sette giorni, accompagnata da un header x-litespeed-tag con le etichette della pagina.
Sette giorni sembrano tanti, ma il trucco sta proprio in quelle etichette: quando pubblico o modifico un articolo, LiteSpeed svuota in automatico solo le pagine collegate, come l’articolo stesso, la home e gli archivi interessati, invece di buttare via tutta la cache. È lo stesso principio di last_changed applicato all’HTML, e funziona finché il codice di temi e plugin non mostra dati che cambiano senza passare dal salvataggio di un contenuto, come un prezzo letto da un servizio esterno o un contatore.
Una cache che non sai invalidare non è un’ottimizzazione, è un bug con la data di scadenza lontana.
Cosa escludere dalla page cache
Le pagine personali non vanno mai servite dalla page cache condivisa: carrello, checkout, area riservata e tutto ciò che dipende dall’utente autenticato. I plugin seri le escludono da soli, ma se usi form, nonce o contenuti personalizzati in pagine pubbliche conviene verificarlo con una finestra anonima e uno sguardo agli header di risposta.
Cache del browser e CDN
La cache del browser si controlla con l’header Cache-Control, e la regola d’oro è distinguere i file versionati dall’HTML. CSS e JavaScript caricati da WordPress hanno di solito un parametro ?ver= che cambia a ogni aggiornamento, quindi possono avere una durata lunga, mentre l’HTML è meglio che venga rivalidato spesso: non a caso su questo sito le pagine rispondono con cache-control: public, max-age=0, mentre la copia lunga resta sul server, dove la posso svuotare quando voglio.
# File statici versionati: possono restare a lungo nel browser e nella CDN
Cache-Control: public, max-age=31536000, immutable
# HTML: il browser rivalida a ogni visita, la CDN può tenerlo un'ora
Cache-Control: public, max-age=0, s-maxage=3600Secondo MDN, immutable indica che la risposta non cambierà finché è fresca ed è pensato proprio per i file con un indirizzo versionato, mentre s-maxage vale solo per le cache condivise come le CDN. Sulla CDN c’è poi un dettaglio che sorprende molti: Cloudflare, per impostazione predefinita, non mette in cache l’HTML ma solo i file statici, quindi per accelerare anche le pagine servono regole dedicate o soluzioni specifiche per WordPress; ne parlo nell’articolo sulle CDN per WordPress nel 2026.
Tirando le somme
La cache in WordPress funziona bene quando ogni livello fa il suo lavoro: browser e CDN per i file statici versionati, page cache per l’HTML dei visitatori anonimi, object cache persistente per tutto ciò che resta dinamico, transient per i calcoli costosi del tuo codice. Il criterio per scegliere non è «più cache possibile», ma sapere per ogni livello chi lo svuota e quando; se non sai rispondere a questa domanda, prima o poi un cliente ti chiamerà per dirti che la modifica non si vede.
Per approfondire ti consiglio la documentazione ufficiale sulle Transients API e sulla classe WP_Object_Cache, e poi un esercizio pratico: apri gli strumenti per sviluppatori del browser e guarda gli header di risposta del tuo sito, perché raccontano molto più di qualunque pannello di configurazione. Altri articoli su questi temi li trovi nella categoria WordPress. Scrivimi nei commenti quale combinazione di cache usi e quali sorprese ti ha riservato, e se l’articolo ti è stato utile condividilo con chi sta ancora svuotando la cache a mano ogni volta che corregge un refuso.
E se preferisci non fare tutto da solo, puoi prenotare una call conoscitiva gratuita oppure scrivermi passando dalla pagina dei contatti: ne parliamo con calma, partendo dal tuo sito e non da una checklist generica.



[…] WordPress e qualità: sistemi di cache manuali | .:: Maurizio Pelizzone ::. […]