Nel 2026, i bundle JavaScript delle applicazioni moderne continuano a crescere del 6-9% annuo (sì, ancora), spingendo Core Web Vitals e TTFB oltre soglie sempre più scomode. Con l'arrivo di RFC 9842 — Compression Dictionary Transport e la beta pubblica di Cloudflare prevista per il 30 aprile 2026, finalmente abbiamo una soluzione nativa: i Compression Dictionaries. In parole povere, questa tecnica permette di comprimere un bundle da 272 KB fino a 2,6 KB — una riduzione del 97% rispetto a Gzip — riutilizzando la versione precedente del file, quella che il browser ha già in cache, come dizionario di compressione.
Onestamente? È una delle novità più interessanti degli ultimi anni sul fronte del web transport. In questa guida vediamo come funziona la compressione condivisa, come configurare gli header Use-As-Dictionary e Available-Dictionary, come generare dizionari con i codec dcb (Dictionary-Compressed Brotli) e dcz (Dictionary-Compressed Zstandard), e come mettere insieme il tutto su Nginx, Cloudflare Workers e altre CDN moderne.
Cosa sono i Compression Dictionaries
Un compression dictionary è — in sostanza — un file di riferimento condiviso tra client e server che fornisce all'algoritmo di compressione un elenco precostruito di sequenze di byte comuni. Invece di comprimere una risposta HTTP da zero, il server può dire al client: "hai già questa parte — ecco solo le differenze".
Il risultato è una forma di delta compression. Quando distribuisci la versione 1.8.3 di un framework e l'utente ha ancora in cache la versione 1.7.9, il browser scarica solo il delta tra le due. Nei test reali, questo trasforma un bundle di 172 KiB in appena 4 KiB, mantenendo identico il contenuto finale. Non è magia, è semplicemente usare ciò che già abbiamo.
Perché nel 2026 è diventato critico
- Traffico agentico in crescita: i crawler AI rappresentano ormai quasi il 10% delle richieste di Cloudflare (marzo 2026, +60% YoY). Ogni re-fetch di una risorsa aggiornata è banda sprecata.
- Deploy continui: le pipeline CI/CD moderne rilasciano decine di volte al giorno. Senza dizionari, ogni piccola modifica invalida la cache e costringe a riscaricare l'intero bundle.
- Core Web Vitals: LCP e INP dipendono direttamente dai tempi di download delle risorse critiche. Ridurre un bundle da 92 KB a 2,6 KB libera il main thread molto prima — e Google se ne accorge.
Come funziona: Brotli condiviso (dcb) vs Zstandard condiviso (dcz)
RFC 9842 estende due algoritmi di compressione popolari: Brotli (dalla versione 1.1.0) e Zstandard. Gzip resta fuori dal gioco, perché non gestisce dizionari esterni di grandi dimensioni.
| Codec | Content-Encoding | Algoritmo base | Dizionario built-in |
|---|---|---|---|
dcb | Dictionary-Compressed Brotli | Brotli 1.1+ | Sì (combinato con esterno) |
dcz | Dictionary-Compressed Zstandard | Zstandard | No (solo esterno) |
Uno stream dcz è composto da un header fisso di 40 byte (8 byte di signature + 32 byte di hash SHA-256 del dizionario) seguito dallo stream Zstandard compresso. Compatto, ma specifico: se un middleware tocca anche solo un byte dell'header, la decompressione fallisce.
Il flusso di negoziazione HTTP
La negoziazione avviene in tre fasi. Vediamole una alla volta.
Fase 1: il server annuncia che una risorsa può essere usata come dizionario
HTTP/2 200 OK
Content-Type: application/javascript
Cache-Control: public, max-age=31536000, immutable
Use-As-Dictionary: match="/assets/app-*.js", match-dest=("script")
Content-Encoding: br
(bundle compresso con Brotli tradizionale)
L'header Use-As-Dictionary dice al browser, in sostanza: "conserva questa risposta come dizionario per future richieste che corrispondono al pattern /assets/app-*.js". Il client memorizza la risorsa insieme al suo hash SHA-256.
Fase 2: il client richiede una nuova versione e dichiara il dizionario disponibile
GET /assets/app-v2.js HTTP/2
Accept-Encoding: gzip, br, zstd, dcb, dcz
Available-Dictionary: :a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e:
Dictionary-ID: "v1.8.3"
Il valore di Available-Dictionary è un Structured Field Byte Sequence contenente l'hash SHA-256 del dizionario. Il client invia sempre un solo hash, quello della miglior corrispondenza disponibile — non è una lista.
Fase 3: il server risponde con contenuto delta-compresso
HTTP/2 200 OK
Content-Type: application/javascript
Content-Encoding: dcb
Vary: Accept-Encoding, Available-Dictionary
(header 40 byte + stream Brotli compresso contro il dizionario)
Nota critica (e non esagero): il Vary header deve includere sia Accept-Encoding sia Available-Dictionary, altrimenti la CDN finirà per servire la stessa risposta a client diversi, corrompendo le cache intermedie. È l'errore numero uno, e l'ho visto fare più volte del dovuto.
Guida pratica: comprimi il tuo primo bundle con dcb
Per sperimentare localmente ti serve la toolchain di Brotli 1.1.0+ oppure il CLI di Pat Meenan per Compression Dictionary Transport.
Passo 1: genera un dizionario dalla versione precedente
#!/bin/bash
# Scarica la versione precedente del bundle
curl -o app-v1.js https://example.com/assets/app-v1.js
# Calcola l'hash SHA-256 (serve per Available-Dictionary)
DICT_HASH=$(openssl dgst -sha256 -binary app-v1.js | xxd -p -c 64)
echo "Dictionary SHA-256: $DICT_HASH"
Passo 2: comprimi la nuova versione usando il dizionario
# Brotli 1.1.0+ con opzione --dictionary
brotli --dictionary=app-v1.js --quality=11 --output=app-v2.js.br app-v2.js
# Oppure Zstandard con dizionario
zstd --train-from-file=app-v1.js -o dict.zstd
zstd -19 -D app-v1.js app-v2.js -o app-v2.js.zst
Passo 3: verifica i risparmi
$ ls -lh app-v2.js*
-rw-r--r-- 1 user user 272K app-v2.js
-rw-r--r-- 1 user user 84K app-v2.js.br # Brotli classico
-rw-r--r-- 1 user user 2.6K app-v2.js.dcb # Con dizionario condiviso
Nel test interno di Cloudflare, un bundle di 272 KB passa da 92,1 KB (Gzip) a 2,6 KB con DCZ. Una riduzione del 97% su un singolo deploy. La prima volta che l'ho visto in console, ho pensato che fosse un bug.
Configurazione Nginx con modulo Brotli
Nginx 1.27+ con il modulo ngx_brotli può servire risorse dcb se pre-compresse a build time. Il supporto runtime è in roadmap, ma per ora funziona bene così. Ecco una configurazione di passthrough:
http {
map $http_accept_encoding $supports_dcb {
~*dcb 1;
default 0;
}
server {
listen 443 ssl http2;
server_name example.com;
location ~* \.js$ {
# Serve .dcb se il client lo supporta E ha un dizionario
if ($supports_dcb = 1) {
set $encoding_type "dcb";
}
add_header Vary "Accept-Encoding, Available-Dictionary";
add_header Use-As-Dictionary 'match="/assets/app-*.js", match-dest=("script")';
try_files $uri.dcb $uri.br $uri =404;
}
types {
application/javascript js;
}
}
}
Cloudflare Workers: esempio end-to-end
Dal 30 aprile 2026 Cloudflare offrirà passthrough automatico degli header di dictionary compression. Nel frattempo, puoi orchestrare tutto tu stesso con un Worker:
export default {
async fetch(request, env) {
const url = new URL(request.url);
const acceptEncoding = request.headers.get("Accept-Encoding") || "";
const availableDict = request.headers.get("Available-Dictionary");
// Se il client supporta dcb E ha un dizionario in cache
if (acceptEncoding.includes("dcb") && availableDict) {
const dictHash = availableDict.replace(/[:\s]/g, "");
const dcbKey = `${url.pathname}?dict=${dictHash}`;
// Cerca la versione pre-compressa contro quel dizionario
const dcbAsset = await env.ASSETS.get(dcbKey, { type: "arrayBuffer" });
if (dcbAsset) {
return new Response(dcbAsset, {
headers: {
"Content-Type": "application/javascript",
"Content-Encoding": "dcb",
"Vary": "Accept-Encoding, Available-Dictionary",
"Cache-Control": "public, max-age=31536000, immutable"
}
});
}
}
// Fallback a Brotli standard + Use-As-Dictionary
const response = await env.ASSETS.fetch(request);
const headers = new Headers(response.headers);
headers.set("Use-As-Dictionary",
`match="${url.pathname.replace(/-[a-z0-9]+\.js$/, "-*.js")}", match-dest=("script")`);
headers.set("Vary", "Accept-Encoding, Available-Dictionary");
return new Response(response.body, {
status: response.status,
headers
});
}
};
Match pattern: come targetizzare i file giusti
L'attributo match di Use-As-Dictionary supporta glob semplici. Qualche esempio pratico:
match="/product/*"— qualsiasi URL sotto/product/match="/app/*/main.js"— filemain.jsin qualsiasi sottocartella di/app/match="/assets/vendor-*.js", match-dest=("script")— limita al destination script, evitando che CSS o HTML riusino il dizionario per sbaglio
La restrizione match-dest è essenziale (davvero, non saltarla): senza di essa un'immagine o un file HTML potrebbero innescare una richiesta dictionary-compressed, complicando cache e debugging in modo poco divertente.
Come misurare l'impatto sui Core Web Vitals
I test Cloudflare su richieste reali mostrano numeri piuttosto chiari:
- Cache miss: DCZ in 31ms contro Gzip in 166ms — miglioramento dell'81%
- Cache hit: DCZ in 16ms contro Gzip in 143ms — miglioramento dell'89%
- LCP: riduzione media di 200-400ms per pagine con bundle critico >100KB
- INP: impatto indiretto positivo grazie al parsing JavaScript più veloce
Per tracciare l'efficacia in produzione, usa il Server-Timing header insieme all'API PerformanceResourceTiming:
// Invia metriche custom con il Content-Encoding effettivo
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.includes("/assets/")) {
const encoding = entry.contentEncoding || "unknown";
const transferKB = (entry.transferSize / 1024).toFixed(1);
const decodedKB = (entry.decodedBodySize / 1024).toFixed(1);
navigator.sendBeacon("/analytics/compression", JSON.stringify({
url: entry.name,
encoding,
transferKB,
decodedKB,
ratio: (entry.transferSize / entry.decodedBodySize).toFixed(3)
}));
}
}
}).observe({ type: "resource", buffered: true });
Supporto browser e fallback
Al momento della scrittura (aprile 2026):
- Chrome 130+ (ottobre 2024) — supporto completo
dcbedcz - Edge 130+ — supporto completo
- Firefox — in sviluppo, tracciato nel bug 1905898
- Safari — nessun annuncio ufficiale (come spesso accade)
Il fallback, per fortuna, è automatico e gratuito: se il client non annuncia dcb/dcz in Accept-Encoding, il server risponde con Brotli o Gzip standard. Puoi anche ispezionare lo stato dei dizionari in Chrome visitando chrome://net-internals/#sharedDictionary — una pagina che uso spesso quando il debug in produzione va storto.
Errori comuni da evitare
- Dimenticare
Vary: Available-Dictionary: le CDN intermedie serviranno risposte delta-compresse a client che non hanno il dizionario, causando errori di decompressione. Questo è il bug numero uno, ripeto. - Usare dizionari con
Cache-Control: no-store: il browser scarica il dizionario, lo scarta e poi lo riscarica — raddoppiando il costo di rete. Il peggio dei due mondi. - Pattern match troppo ampi:
match="/*"forza il browser a inviareAvailable-Dictionarysu ogni richiesta, gonfiando gli header inutilmente. - Non versionare i file: senza hash o version nel filename, il browser non può distinguere versioni diverse e invalida il dizionario troppo presto.
- Middleware HTTP non conformi: alcuni WAF ricomprimono la risposta, rompendo l'header 40-byte di
dcz. Verifica sempre concurl -H "Accept-Encoding: dcb, dcz" -vprima di dichiarare vittoria.
Quando NON usare Compression Dictionaries
- Contenuti che cambiano completamente: un'immagine JPEG o un bundle rigenerato da zero non traggono benefici — il delta sarà grande quanto il file intero.
- Siti con poche risorse statiche cacheabili: blog puramente HTML con TTL brevi non ammortizzano il costo di memorizzare il dizionario.
- Ambienti non-HTTPS: RFC 9842 richiede contesti sicuri per prevenire attacchi side-channel sui middle-box.
- Risposte personalizzate utente: contenuti che variano per cookie o header di autenticazione non sono dizionari sicuri da condividere.
Domande frequenti (FAQ)
Quanto spazio occupano i dizionari nella cache del browser?
Il browser usa lo stesso storage della HTTP cache standard. Chrome limita ciascun dizionario a 100 MB e applica politiche LRU globali. In pratica, un bundle JavaScript versionato (<500 KB) non genera problemi di memoria — nemmeno su dispositivi mobile di fascia bassa.
Come interagiscono i Compression Dictionaries con HTTP/3?
Sono completamente compatibili. Il codec dcb/dcz opera a livello applicativo, indipendente dal trasporto. Anzi, abbinati a QUIC eliminano una delle cause più comuni di bloat: i bundle grandi su connessioni mobile lente.
Posso usare Compression Dictionaries con Webpack o Vite?
Sì, tramite plugin CLI post-build. Gli script Node.js di Pat Meenan (compression-dictionary-notes su GitHub) integrano la generazione di .dcb e .dcz nella pipeline. Vite 6+ ha un plugin sperimentale @vitejs/plugin-compression-dictionary che vale la pena provare.
Qual è la differenza rispetto alla cache HTTP tradizionale?
La cache HTTP rispedisce l'intero file quando scade o cambia. I Compression Dictionaries inviano solo il delta rispetto alla versione precedente, anche quando l'URL è nuovo. È complementare, non sostitutiva — le due tecniche lavorano bene insieme.
È sicuro dal punto di vista privacy?
RFC 9842 impone same-origin per l'uso di dizionari, impedendo attacchi cross-origin. In più, Chrome disabilita la funzionalità in navigazione privata e con cookie di terze parti bloccati. L'hash SHA-256 garantisce che client e server usino esattamente lo stesso dizionario — nessuna ambiguità.
Conclusione
I Compression Dictionaries rappresentano, a mio avviso, il salto più importante nelle performance di trasporto HTTP dal rilascio di Brotli nel 2015. Con rapporti di compressione che superano tranquillamente il 90% sui deploy incrementali, la possibilità di tagliare LCP di centinaia di millisecondi, e l'arrivo del supporto nativo su Cloudflare entro fine aprile 2026, è davvero il momento di sperimentare.
Inizia dai tuoi bundle JavaScript versionati: sono i candidati ideali per il primo rollout, con benefici immediati e fallback gratuito per i browser non compatibili. Se hai già Brotli in produzione, il passo successivo è più piccolo di quanto pensi.