
# Cache busting: perché il tuo ?v= non serve a niente

La nostra navigazione è comparsa in produzione senza uno stile per buona parte di un pomeriggio. L'HTML era giusto, il file CSS sul server era giusto e il deploy era andato a buon fine. È stato diagnosticato male due volte prima che qualcuno guardasse nel punto giusto.

`app.css` usciva senza nessun `?v=`. Cloudflare lo teneva in cache quattro ore, i browser molto più a lungo. Quella release rinominava un gruppo di classi CSS, quindi l'HTML nuovo arrivava a un browser che conservava ancora il foglio di stile della build precedente: nessuna classe del markup corrispondeva più a niente.

Quello che tradiva una cache e non un deploy rotto è esattamente ciò che lo rendeva difficile da vedere: un ricaricamento forzato risolveva, e chi apriva il sito per la prima volta lo vedeva bene. Una cache fredda è indistinguibile da un deploy funzionante, perciò nessuno che provasse con un profilo pulito ha mai riprodotto il problema.

`app.js` un `?v=` ce l'aveva, ed era peggio che non averlo. Interpolava la versione dal `package.json`:

```html
<script src="/js/app.js?v=<%= appVersion %>" defer></script>
```

Quel numero non si muove tra una release e l'altra, e noi pubblichiamo molto più spesso di quanto lo incrementiamo. Il JS portava un token che, per chiunque leggesse il template, sembrava cache busting e non invalidava niente. Lo stesso bug del CSS, a una release dallo scattare, con un'esca davanti.

## Fai l'hash del file

Un token di cache busting ha un solo compito: cambiare quando cambia il file, e in nessun altro momento. L'unico valore che lo fa deriva dai byte del file stesso.

```js
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';

function assetVersion(relativePath) {
  const buf = readFileSync(join(__dirname, 'public', relativePath));
  return createHash('sha1').update(buf).digest('hex').slice(0, 8);
}

const cssVersion = assetVersion('css/app.css');
```

Letto una volta sola all'avvio: sono output di build, non possono cambiare sotto un processo in esecuzione. Otto caratteri esadecimali bastano e avanzano, e SHA-1 qui va benissimo, perché stai evitando una collisione accidentale fra due versioni di un tuo file, non un attaccante che sceglie il contenuto.

E il template smette di mentire:

```html
<link rel="stylesheet" href="/css/app.css?v=<%= cssVersion %>">
```

I tre token a cui si ricorre al posto suo:

**Un numero di versione** (`?v=1.4.2`) cambia quando una persona si ricorda di cambiarlo, che non è la stessa cosa di quando cambia il file. Era il nostro `app.js`.

**Un timestamp o un id di build** cambia davvero a ogni deploy, quindi non serve mai roba vecchia, ma cambia anche quando non è cambiato niente. Ogni deploy butta via il CSS in cache di tutti i visitatori e la copia di ogni nodo del CDN, che si sia mosso un byte o no, e una release che ha toccato solo il backend costa comunque un download a ogni utente.

**Un valore casuale** (`?v=<%= Math.random() %>`, e c'è chi lo scrive) è un URL nuovo a ogni caricamento di pagina. Non invalida la cache, la spegne per sempre e per tutti.

Solo l'hash del contenuto azzecca entrambe le metà: mai roba vecchia, e mai un nuovo download quando non è cambiato niente.

## Query string o nome del file

`app.a1b2c3d4.css` fa la stessa promessa con l'hash nel nome invece che nella query string. Serve però un passo di build che riscriva ogni riferimento, ed è per questo che i progetti con bundler di solito ce l'hanno e quelli renderizzati sul server di solito no.

L'argomento storico a favore del nome è che alcune cache ignorano la query string quando calcolano la chiave, e allora il `?v=` resta decorazione. È ancora un'impostazione che puoi attivare per sbaglio, visto che il livello di cache di Cloudflare ha un'opzione «ignore query string», e se è accesa tutti i `?v=` del tuo sito smettono di funzionare senza dire niente. Controlla prima di fidarti.

## `immutable` è una promessa sull'URL, non sul file

`Cache-Control: public, max-age=31536000, immutable` dice al browser di non rivalidare, nemmeno quando l'utente ricarica. È vero solo se l'URL è un indirizzo di contenuto, e messa così la regola si scrive da sola:

```js
if (/\.(css|js)$/.test(name)) {
  const versioned = /[?&]v=/.test(url) || filePath.includes('/vendor/');
  res.setHeader('Cache-Control', versioned
    ? 'public, max-age=31536000, immutable'
    : 'public, max-age=3600');
}
```

Lo stesso file richiesto senza il token prende un'ora. Il token è la promessa, e niente va tenuto in cache per un anno su una promessa che nessuno ha fatto.

Verifica che il tuo middleware per gli statici non te la sovrascriva. `@fastify/static` manda `max-age=0` di default; se imposti header senza disattivare il suo `cacheControl`, ricostruisce l'header da quel valore e sostituisce il tuo in silenzio. L'header è ancora lì, sembra ancora voluto, ed è sbagliato.

I font li marchiamo immutable per il nome. Una build di subset si sostituisce rinominando, non modificando, quindi il nome è già l'indirizzo del contenuto. Le librerie vendorizzate uguale: `alpine-3.14.8.min.js` porta la versione nel nome e nessuno la modifica sul posto.

Le immagini prendono una settimana, non un anno. Non sono versionate, e un logo o una favicon vengono davvero sostituiti allo stesso indirizzo: una settimana è abbastanza da servire a qualcosa e abbastanza corta da rimediare a uno sbaglio.

## Il file che deve restare a vita breve

Serviamo uno script di badge che gira dentro le pagine di altri. Non controlliamo né il loro HTML né l'URL che incorporano. Un anno di `immutable` lì significa un bug che non possiamo raggiungere per un anno: nessun modo di invalidare, nessun modo di chiedere a ogni sito che ha il tag di cambiarlo. Prende un'ora, ed è la scelta giusta anche se il file non cambia quasi mai.

Tutto ciò che vive su un URL che non puoi versionare funziona così. La durata in cache la decide quanto in fretta devi poterlo correggere, non quanto spesso cambia.

## Verifica

Il token nell'HTML corrisponde al file su disco?

```bash
curl -s https://iltuosito.com/ | grep -o 'app\.css?v=[a-f0-9]*'
shasum -a 1 public/css/app.css | cut -c1-8
```

La stessa stringa, altrimenti il tuo HTML punta a qualcosa che non descrive. Se il token resta fermo su due deploy che hanno entrambi toccato il CSS, hai il nostro `app.js`.

Poi chiedi alla risorsa cosa risponde:

```bash
curl -sI 'https://iltuosito.com/css/app.css?v=a1b2c3d4' | grep -i 'cache-control\|last-modified\|cf-cache-status'
```

`last-modified` è quello che smaschera un nodo con la copia vecchia. Se è precedente al tuo ultimo deploy di quel file, qualcosa tra il tuo server e il browser conserva ancora la versione vecchia, e a decidere se questo ti danneggia è il token nel tuo HTML.

[Analizza il tuo sito](/it) · [Checklist di preparazione all'IA](/it/tools/ai-readiness-checklist) · [Cosa misuriamo](/it/about)
