⌨️ Sviluppo web

Caching HTTP: Cache-Control, ETag e versioning degli asset

Come funziona la cache HTTP: Cache-Control, validazione con ETag, nomi di file con hash e una strategia sicura per HTML, CSS, JavaScript e API.

La richiesta più veloce è quella che non viene fatta. La cache HTTP permette al browser, e ai server intermedi come i CDN, di riutilizzare risposte già ricevute invece di scaricarle di nuovo. Configurata bene, rende istantanee le visite successive e riduce il carico sul server. Configurata male, produce il problema opposto e più fastidioso: utenti che vedono una versione vecchia del sito dopo un aggiornamento.

Due meccanismi: freschezza e validazione

La cache HTTP si basa su due idee distinte.

  1. Freschezza: per un certo periodo la risposta in cache è considerata valida e viene usata senza contattare il server.
  2. Validazione: scaduta la freschezza, il browser chiede al server "è cambiato qualcosa?". Se no, il server risponde 304 Not Modified senza corpo, e il browser riusa la copia locale.

La validazione fa comunque una richiesta, ma molto leggera. La freschezza evita del tutto la richiesta.

L'header Cache-Control

È l'header principale per controllare la cache. Le direttive più importanti:

Direttiva Significato
max-age=N la risposta è fresca per N secondi
s-maxage=N come max-age, ma solo per cache condivise (CDN, proxy)
no-cache si può memorizzare, ma va sempre validata prima dell'uso
no-store non memorizzare affatto
public può essere memorizzata anche da cache condivise
private solo nella cache del browser dell'utente
immutable il contenuto non cambierà mai durante la freschezza
must-revalidate scaduta la freschezza, non usare la copia senza validarla
stale-while-revalidate=N per N secondi usa la copia scaduta mentre la aggiorna in background

L'equivoco più comune riguarda no-cache: non significa "non mettere in cache", ma "metti in cache e chiedi sempre conferma". Per vietare la memorizzazione si usa no-store.

Validazione con ETag e Last-Modified

Per validare, il server fornisce un identificativo della versione:

HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "5f3a-1b2c"

Alla richiesta successiva il browser lo rimanda:

GET /index.html
If-None-Match: "5f3a-1b2c"

Se l'ETag coincide, la risposta è 304 senza contenuto. In alternativa all'ETag esiste Last-Modified, con la richiesta condizionale If-Modified-Since, basato sulla data di modifica. Molti server e hosting statici generano entrambi automaticamente.

La strategia che funziona: asset con hash e HTML sempre validato

Il problema di fondo è questo: vuoi che CSS, JavaScript e immagini restino in cache a lungo, ma vuoi anche che un aggiornamento arrivi subito. La soluzione standard è il versioning tramite nome di file.

Gli strumenti di build generano nomi che contengono un'impronta (hash) del contenuto:

<link rel="stylesheet" href="/assets/style.3f9a2c1b.css">
<script src="/assets/app.8d41e0a7.js" defer></script>

Quando il file cambia, cambia anche l'hash e quindi l'URL. Il vecchio file in cache non viene più richiesto: l'HTML punta già a quello nuovo. Per questo gli asset con hash possono avere una cache lunghissima:

Cache-Control: public, max-age=31536000, immutable

L'HTML, invece, è il punto d'ingresso che contiene i riferimenti agli asset: deve essere sempre aggiornato. Per l'HTML conviene:

Cache-Control: no-cache

oppure una freschezza molto breve. Così ogni visita controlla se la pagina è cambiata (con una richiesta leggera grazie all'ETag) e, in caso di aggiornamento, scarica il nuovo HTML con i nuovi riferimenti.

Il caso degli asset senza hash

Se un file ha un nome fisso, come /logo.png o /style.css, una cache di un anno è pericolosa: dopo una modifica gli utenti continuerebbero a vedere la vecchia versione. Per questi file usa una freschezza moderata (per esempio qualche ora) unita alla validazione, oppure introduci il versioning.

Riepilogo per tipo di risorsa

Risorsa Cache-Control consigliato
HTML no-cache (con ETag)
CSS/JS/font con hash nel nome public, max-age=31536000, immutable
Immagini con nome fisso public, max-age=86400 più validazione
Risposte API personali private, no-cache oppure no-store
Dati sensibili (area riservata) no-store

Cache condivise e dati personali

Un CDN o un proxy serve la stessa copia a più utenti. Se una risposta contiene dati personali (il nome dell'utente, il contenuto del carrello) e viene memorizzata da una cache condivisa, un altro utente potrebbe riceverla. Per le risposte che dipendono dall'utente usa sempre private o no-store, e verifica la configurazione del CDN, che a volte applica regole proprie.

L'header Vary indica alle cache quali header della richiesta cambiano la risposta, per esempio Vary: Accept-Encoding per le versioni compresse. Usalo con parsimonia: valori come Vary: Cookie riducono drasticamente l'efficacia della cache condivisa.

Come verificare

  1. Negli strumenti per sviluppatori, scheda Rete, osserva la colonna dimensione: "memory cache" o "disk cache" indicano una risposta servita localmente, mentre 304 indica una validazione riuscita.
  2. Disattiva temporaneamente l'opzione che disabilita la cache negli strumenti per sviluppatori, altrimenti non vedrai il comportamento reale.
  3. Controlla gli header con curl -I, anche per le risposte servite dal CDN.

In sintesi

  • La cache combina freschezza (nessuna richiesta) e validazione (richiesta leggera con risposta 304).
  • no-cache significa "valida sempre", no-store significa "non memorizzare".
  • Dai agli asset con hash nel nome una cache di un anno con immutable.
  • Mantieni l'HTML sempre validato, così gli aggiornamenti arrivano subito.
  • Non permettere alle cache condivise di memorizzare risposte con dati personali.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche