In quasi tutti i siti le immagini sono la parte più pesante della pagina, e molto spesso l’elemento più grande del primo schermo è proprio una foto: quella del prodotto, dell’articolo, della copertina. Ridurne il peso significa accorciare il download dell’elemento che determina l’LCP, consumare meno banda sui dispositivi mobili e alleggerire server e CDN. I formati moderni, WebP e AVIF, permettono di ottenere la stessa qualità percepita con file molto più leggeri del JPEG. Il problema è come servirli senza escludere chi non li supporta e senza rompere le cache. In questo articolo vediamo il serving condizionale, cioè la stessa URL che restituisce il formato migliore per ciascun browser, e l’intestazione che lo rende sicuro: Vary: Accept.
WebP e AVIF: quanto si risparmia
WebP, introdotto da Google, comprime le fotografie in genere del 25-35% meglio del JPEG a parità di qualità percepita, e supporta trasparenza e animazione. AVIF, derivato dal codec video AV1, va oltre: su molte fotografie arriva a dimezzare il peso rispetto al JPEG, gestisce meglio sfumature e cieli e supporta ampie gamme di colore. Sono valori indicativi, perché il risultato dipende dal contenuto dell’immagine e dalle impostazioni di qualità, ma la direzione è costante: a parità di aspetto, AVIF è di solito il più leggero, WebP una via di mezzo, JPEG il più pesante.
Tutti i principali browser aggiornati supportano WebP, e la grande maggioranza supporta anche AVIF. Restano però in circolazione dispositivi e versioni datate che non lo fanno, oltre a client che non sono browser: programmi di posta, anteprime dei social, strumenti di archiviazione. Servire solo AVIF sarebbe rischioso; servire solo JPEG significa rinunciare al risparmio. Serve un modo per dare a ciascuno il formato migliore che è in grado di leggere.
Due strade: il markup o il server
La prima strada è lasciare la scelta al browser tramite il markup, con l’elemento <picture>. Si elencano le versioni disponibili in ordine di preferenza e il browser usa la prima che supporta:
<picture>
<source type="image/avif" srcset="/img/prodotto.avif">
<source type="image/webp" srcset="/img/prodotto.webp">
<img src="/img/prodotto.jpg" width="1200" height="800" alt="…">
</picture>
È una soluzione robusta e trasparente per le cache, perché ogni formato ha la sua URL. Ma richiede di modificare il markup di ogni immagine: temi, page builder, contenuti inseriti negli anni nell’editor, immagini dei prodotti generate da plugin. Su siti grandi e stratificati è spesso impraticabile.
La seconda strada è il serving condizionale, detto anche negoziazione del contenuto: il markup resta invariato, con il solito /img/prodotto.jpg, e il server sceglie quale file restituire in base a ciò che il browser dichiara di supportare. È l’approccio che permette di ottimizzare un intero sito esistente senza toccare una riga di HTML, ed è quello che approfondiamo qui.
Come funziona la negoziazione del contenuto
Ogni volta che il browser richiede un’immagine invia l’intestazione Accept, con l’elenco dei formati che è in grado di mostrare. Un browser recente dichiara per esempio qualcosa come image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8. Il server legge questa intestazione, verifica se esiste una versione AVIF o WebP dell’immagine richiesta e restituisce la più leggera tra quelle supportate. L’indirizzo non cambia: cambiano il contenuto e l’intestazione Content-Type, che dichiara il formato effettivo.
Fin qui tutto sembra semplice. Il punto delicato è che tra il browser e il server, quasi sempre, ci sono delle cache: la CDN, un reverse proxy come Varnish, a volte proxy aziendali. E una cache, per definizione, conserva una risposta e la riutilizza per le richieste successive alla stessa URL.
Vary: Accept, spiegato bene
Una cache identifica ogni risposta con una chiave, che in origine è semplicemente la URL. Se la stessa URL può produrre risposte diverse a seconda di come è fatta la richiesta, la cache deve saperlo, altrimenti riutilizzerà la prima risposta ricevuta per tutti. L’intestazione Vary serve esattamente a questo: è una risposta del server che dice alle cache “questo contenuto dipende anche da queste intestazioni della richiesta”. Con Vary: Accept il server comunica che la risposta cambia in base all’intestazione Accept, e le cache devono includerla nella chiave.
Vediamo che cosa succede senza. Un browser moderno chiede /img/prodotto.jpg, il server risponde con la versione AVIF e la CDN la conserva, associata alla sola URL. Poco dopo arriva un browser che non supporta AVIF e chiede la stessa URL: la CDN trova una copia valida e la restituisce. Risultato: un’immagine che quel browser non sa decodificare, cioè un riquadro vuoto. Con Vary: Accept, invece, la CDN conserva la risposta AVIF associata alla combinazione di URL e intestazione Accept del primo browser; quando arriva il secondo, con un Accept diverso, la chiave non corrisponde e la richiesta prosegue verso il server, che restituisce il JPEG.
Tre dettagli fanno la differenza tra una configurazione corretta e una che funziona solo a volte:
- Vary va inviato su tutte le varianti, compreso il JPEG. Se la risposta JPEG arriva senza
Vary: Accept, una cache la considera valida per chiunque e la servirà anche ai browser che supporterebbero AVIF, annullando il risparmio. È l’errore più comune. - Il Content-Type deve dichiarare il formato vero. Un file AVIF servito come
image/jpegpuò essere rifiutato o gestito male da browser e intermediari. - Vary vale anche per la validazione. Le richieste condizionali con ETag, di cui abbiamo parlato nell’articolo sul codice 304, devono riferirsi alla variante giusta: ogni formato va trattato come una risorsa distinta, con il proprio identificatore di versione.
Il rovescio: la frammentazione della cache
Vary: Accept risolve il problema della correttezza, ma ne crea uno di efficienza. L’intestazione Accept non è uguale per tutti: cambia tra browser, tra versioni dello stesso browser, tra tipi di richiesta. Una cache che usa il valore completo come parte della chiave finisce per conservare decine di copie della stessa immagine, una per ogni variante dell’intestazione, anche se i file realmente diversi sono solo tre. Ogni nuova combinazione è un MISS, il tasso di HIT crolla e l’origine riceve molte più richieste del necessario.
La soluzione è normalizzare l’intestazione prima che diventi chiave di cache: ridurre le decine di valori possibili a tre soli, “avif”, “webp” e “jpeg”, in base a ciò che il client supporta davvero. Così ogni immagine ha al massimo tre copie in cache, una per formato.
In Varnish la normalizzazione si fa all’ingresso della richiesta, riscrivendo l’intestazione prima che venga usata per la ricerca in cache. Varnish gestisce Vary in modo nativo, quindi confronterà ormai solo tre valori possibili:
sub vcl_recv {
if (req.url ~ "\.(jpe?g|png)(\?.*)?$") {
if (req.http.Accept ~ "image/avif") {
set req.http.Accept = "image/avif";
} elseif (req.http.Accept ~ "image/webp") {
set req.http.Accept = "image/webp";
} else {
set req.http.Accept = "image/jpeg";
}
}
}
Con le CDN il discorso va verificato caso per caso. Alcune ignorano Vary o lo accettano solo per pochi valori, altre offrono impostazioni dedicate alle immagini che gestiscono internamente formato e chiavi di cache, altre ancora richiedono di includere esplicitamente l’intestazione normalizzata nella chiave. Prima di attivare il serving condizionale dietro una CDN conviene leggere come tratta Vary e verificare il comportamento con richieste reali: è il punto in cui più spesso qualcosa si rompe, come abbiamo visto parlando di Cloudflare e cache.
Un esempio con Nginx
Un modo semplice di implementare il serving condizionale è generare in anticipo, accanto a ogni immagine, le versioni moderne con un suffisso: prodotto.jpg.avif e prodotto.jpg.webp. Nginx sceglie il suffisso in base ad Accept e serve il primo file esistente, ripiegando sull’originale:
map $http_accept $img_suffix {
default "";
"~*image/avif" ".avif";
"~*image/webp" ".webp";
}
location ~* \.(?:jpe?g|png)$ {
add_header Vary Accept always;
try_files $uri$img_suffix $uri =404;
}
# Se mancano nel mime.types della versione in uso:
types {
image/avif avif;
image/webp webp;
}
Le espressioni regolari della map vengono valutate nell’ordine in cui sono scritte, quindi AVIF, se dichiarato, ha la precedenza su WebP. L’intestazione Vary viene aggiunta a ogni risposta del blocco, JPEG compreso, e il Content-Type segue l’estensione del file effettivamente servito. Una configurazione equivalente si può ottenere con Apache e mod_rewrite, verificando l’intestazione Accept e l’esistenza del file.
Come generare le varianti
Le versioni WebP e AVIF possono essere create al caricamento delle immagini, da un plugin o da una procedura del CMS; in fase di build, per i siti statici o i temi; oppure al volo da un servizio di ottimizzazione delle immagini, che le conserva poi in cache. La regola importante è non codificarle a ogni richiesta: la compressione AVIF in particolare è molto più lenta di quella JPEG, e farla in tempo reale senza cache sposterebbe il costo sul TTFB delle immagini.
La qualità va calibrata a vista, confrontando le versioni sulle immagini tipiche del sito: i valori ottimali per una foto di prodotto su fondo bianco non sono gli stessi di un paesaggio o di uno screenshot con testo. E il formato non sostituisce le dimensioni: un AVIF da 2.000 pixel servito a uno smartphone resta sprecato. Il serving condizionale sceglie il formato, mentre srcset e sizes continuano a scegliere la dimensione giusta per lo schermo.
Picture o serving condizionale?
| <picture> | Serving condizionale | |
|---|---|---|
| Modifica del markup | Sì, per ogni immagine | No |
| Chi sceglie il formato | Il browser | Il server, in base ad Accept |
| URL per formato | Distinte | Una sola |
| Configurazione delle cache | Nessuna attenzione particolare | Vary: Accept e normalizzazione |
| Immagini già presenti nei contenuti | Da riscrivere | Ottimizzate subito |
| Rischio principale | Markup non aggiornato | CDN che ignorano Vary |
Le due strade possono anche convivere: <picture> per le immagini controllate dal tema, come la copertina o l’immagine principale, e il serving condizionale per tutto ciò che arriva dai contenuti.
Le insidie da conoscere
- File salvati con l’estensione sbagliata. Chi salva un’immagine dal browser può ottenere un file che si chiama
.jpgma contiene un AVIF, e che alcuni programmi non aprono. - Client che non dichiarano i formati. Anteprime dei social, programmi di posta e strumenti automatici spesso inviano
Accept: */*: devono ricevere il JPEG, ed è proprio ciò che fa una configurazione corretta. - Vary dimenticato su una variante. Basta una risposta senza
Varyper contaminare una cache condivisa. - Chiave di cache con l’Accept completo. Senza normalizzazione il tasso di HIT delle immagini crolla e l’origine si ritrova a servire molto più traffico.
- Codifica al volo senza cache. Convertire in AVIF a ogni richiesta rende le immagini più leggere ma più lente ad arrivare.
Come verificare che funzioni
Il controllo più diretto è simulare browser diversi cambiando l’intestazione Accept e osservare Content-Type, Vary e, se presente, lo stato della cache della CDN:
# Browser che supporta AVIF
curl -sI -H "Accept: image/avif,image/webp,*/*" https://www.iltuosito.it/img/prodotto.jpg \
| grep -iE "content-type|vary|content-length"
# Client che non dichiara formati moderni: deve ricevere il JPEG
curl -sI -H "Accept: */*" https://www.iltuosito.it/img/prodotto.jpg \
| grep -iE "content-type|vary|content-length"
Ripetendo le richieste si verifica che la cache restituisca ogni volta la variante giusta. Nel pannello Network dei DevTools la colonna del tipo mostra il formato ricevuto e quella delle dimensioni il peso effettivo; negli strumenti di laboratorio, la voce sui formati di immagine moderni dovrebbe scomparire. Nei dati degli utenti reali l’effetto si vede su LCP, soprattutto da mobile.
In sintesi
WebP e AVIF riducono in modo consistente il peso delle immagini, e con esso i tempi di caricamento dell’elemento principale della pagina. Il serving condizionale permette di adottarli su un sito esistente senza modificare il markup: la stessa URL restituisce il formato migliore che ciascun browser dichiara di supportare con l’intestazione Accept.
La chiave per farlo in sicurezza è Vary: Accept, presente su ogni variante, che impedisce alle cache di servire un formato a chi non lo supporta. La chiave per farlo in modo efficiente è normalizzare Accept in tre soli valori, così da non frammentare la cache. Con varianti generate in anticipo, CDN verificate e dimensioni responsive, il risultato è lo stesso sito, con immagini identiche agli occhi degli utenti e decine di kilobyte in meno per ogni pagina.