Skip to content
SnapFrom

The Update That Weighs 98 Bytes

· · 9 min read

Ship a one-line fix to a JavaScript bundle and every returning visitor downloads the whole thing again. Not the line. The whole bundle, all 100 KB of it, 99.9% of which is byte-for-byte what is already sitting in their browser cache.

Nobody misconfigured anything. This is what the HTTP cache is: a lookup table keyed on URL. Content-hashed filenames, the practice every bundler defaults to and every performance guide recommends, make that key change on every build precisely so the stale copy can never be served. The cache works exactly as designed, and the design has no way to say “this new URL is the old URL plus four characters.”

There is now a standard that can say that. It became RFC 9842, Standards Track, in September 2025[1], it has shipped in Chromium since Chrome 130[4], and in the bench further down it turns a 34,559-byte download into a 98-byte one.

The handshake, end to end

Three headers and one new content encoding. That is the whole protocol.

A server that wants a response to double as a dictionary for future responses attaches Use-As-Dictionary to it, carrying a URL pattern[2]:

HTTP/2 200
content-type: text/javascript
cache-control: max-age=31536000
use-as-dictionary: match="/js/app.*.js"

The browser stores the bytes it just downloaded and remembers the pattern. Later, when it goes to fetch a URL that matches, it says so in the request: a SHA-256 hash of the dictionary it holds, plus two new tokens in Accept-Encoding[1][2].

GET /js/app.7f3c9a.js
accept-encoding: gzip, br, zstd, dcb, dcz
available-dictionary: :pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=:

The hash is the whole identity check. The spec allows a server to also hand out an opaque id that the client echoes back in a Dictionary-ID header for easier lookup on the server side, but MDN is explicit that this is a convenience and not a substitute: “the server must still check the hash from the Available-Dictionary header”[2]. Only one dictionary may be offered per request - “the client MUST only send a single Available-Dictionary request header with a single hash value for the best available match that it has available”[1].

If the server holds the same dictionary, it compresses the new file against it and answers with Content-Encoding: dcb (Brotli) or dcz (Zstandard). If it does not, it falls back to ordinary br and nothing breaks.

There is a second flow for the case where no existing response is a good dictionary: a dedicated dictionary file, advertised with <link rel="compression-dictionary" href="/dictionary.dat" /> and “downloaded by the browser during idle time”[2]. That is the shape Google Search uses. The version-to-version flow is the one that matters for the deploy problem.

What it is actually worth

Every source that describes this feature quotes a different headline number against a different baseline, which makes them impossible to compare. So here is a bench anyone can rerun: three real libraries from a public CDN, each compressed with Brotli at quality 11 alone, and then compressed again with the previous published release as the dictionary. Run on 16 September 2026, on an Apple M3 Pro under macOS 26.2, with brotli 1.2.0 and zstd 1.5.7.

$ curl -sO https://cdn.jsdelivr.net/npm/vue@3.5.19/dist/vue.runtime.global.prod.js
$ curl -sO https://cdn.jsdelivr.net/npm/vue@3.5.20/dist/vue.runtime.global.prod.js

$ brotli -q 11 -f -o new.br vue-3.5.20.js                      # ordinary Brotli
$ brotli -q 11 -f -D vue-3.5.19.js -o new.dcb vue-3.5.20.js    # against the previous release
version steprawbrotlidcbzstddcz
preact 10.27.1 to 10.28.011,2334,3651274,680119
preact 10.26.9 to 10.28.011,2334,3653484,680354
htmx 2.0.5 to 2.0.651,00714,86316415,787161
htmx 2.0.4 to 2.0.651,00714,8633,13315,7873,353
vue 3.5.19 to 3.5.20100,93834,5596236,19260
vue 3.5.17 to 3.5.20100,93834,55968736,192689
vue 3.4.38 to 3.5.20100,93834,55913,94536,19214,842

All figures in bytes. The three Vue steps span four days, two months and just over a year of upstream development respectively: 3.5.19 was published 21 August 2025, 3.5.20 on 25 August 2025, 3.5.17 on 18 June 2025 and 3.4.38 on 15 August 2024[5].

The top line of Vue is the number in this article’s title. A browser holding 3.5.19 and asking for 3.5.20 needs 62 bytes of compressed payload instead of 34,559. Add the dcb format’s fixed 36-byte header - four magic bytes and the 32-byte SHA-256 digest of the dictionary[1] - and 98 bytes cross the wire. Thirty-two of those bytes are the hash identifying which dictionary was used.

The number the docs do not put on a curve

Look down the Vue column rather than across it.

One patch release back, four days of development: 62 bytes. Two releases back, two months: 687. One minor version back, just over a year: 13,945, which is 40% of plain Brotli and a perfectly ordinary compression result. The saving belongs to the version distance rather than to the feature, and it collapses fast.

That is the fact that decides whether this is worth deploying, and it is the one number no primary source states. RFC 9842 specifies the wire format and says only that delta compression “usually result[s] in significantly smaller responses”[1]. MDN says “an order of magnitude more compression”[2], which understates the adjacent case by more than a factor of fifty and overstates the year-apart case. Cloudflare’s lab figure of 99% against uncompressed comes from “nearly-identical JS bundles across successive deploys”[4], which is the best case by construction. Google Search’s 23% is against a dictionary deliberately rebuilt to keep “pace with frequently changing SRP content that is released multiple times a day”[3] - an automated pipeline exists precisely because the decay above is real.

So the mechanism rewards a specific shape of site: frequent deploys, small diffs, returning visitors who come back before the dictionary they hold goes stale. A site that ships quarterly gets the bottom row of that table.

It is also cheaper to compute

The obvious objection to turning this on is server CPU: Brotli at quality 11 is famously slow, and now you want to run it against a dictionary too.

It runs faster. Same input, same quality level, 20 runs each:

brotli -q 11                   99.9 ms
brotli -q 11 -D prev            8.4 ms
zstd   -19                     20.8 ms
zstd   -19 -D prev              9.7 ms

Twelve times faster for Brotli. The reason is not subtle once you see the output size: the matcher is not searching for redundancy, it is finding the entire file at offset zero in the dictionary and emitting a handful of copy instructions. The work that made quality 11 expensive is work the dictionary has already done.

The specification, MDN’s guide, Chrome’s write-up and Cloudflare’s announcement all present this as a bandwidth optimisation. None of them mentions that the expensive part of your build or your edge compression gets cheaper at the same time.

What it costs, added up

Individually each of these appears in a spec as a sentence. Nobody totals them.

Your cache fragments. A dictionary-compressed response must carry Vary: accept-encoding, available-dictionary[2], which means the cache key now includes which dictionary the client held. Cloudflare, who have to operate this, state the consequence: “responses vary on both encoding and dictionary hash, so every dictionary version creates a separate cache variant…Hit rates drop, storage climbs”[4]. Mid-deploy you have “clients with the old dictionary, clients with the new one, and clients with none”[4], and each population needs its own stored variant.

The dictionary has to still be in cache. “To be considered as a match, the dictionary resource MUST be either fresh or allowed to be served stale”[1]. A visitor whose previous bundle was evicted gets the full download, and you have no way to know how many of your visitors that is.

It is same-origin and HTTPS only. The URL matching algorithm returns false when “the Origin of BASEURL and the Origin of URL are not the same”[1], and “compression dictionary transport MUST only be used in secure contexts (HTTPS)”[1]. Assets on a separate CDN hostname are a different origin unless you go through CORS.

It behaves like a cookie, and browsers treat it like one. This is the constraint with the longest reach. Because the client advertises a hash of content it holds, the RFC is blunt: “it is possible to abuse the dictionary to turn it into a tracking cookie”, so “clients MUST treat dictionaries in the same way that they treat cookies”, including partitioned storage and “clearing the dictionaries whenever cookies are cleared”[1]. MDN carries the practical form: “browsers may restrict this feature when cookies are disabled or when other extra privacy protections are enabled”[2]. The readers most likely to be on a slow connection and most likely to have cleared their storage are the ones least likely to benefit.

Half the browsers do not have it. MDN’s status line is “this feature is not Baseline because it does not work in some of the most widely-used browsers”[2]; Cloudflare’s April 2026 summary is “Chrome 130+ and Edge 130+, with Firefox support in progress”[4]. Every Safari and Firefox visitor takes the ordinary br path, which is fine, but they are outside the saving entirely before any of the conditions above are applied.

Who is actually running it

Google Search is, and published the result: 23% smaller HTML on average across all Chrome users versus standard Brotli, worth “a Largest Contentful Paint (LCP) improvement of 1.7% overall, and up to 9% on high latency networks”[3]. Cloudflare opened a passthrough beta on 30 April 2026 that forwards the headers rather than managing dictionaries for you[4].

Beyond that, thinly. On 16 September 2026 we requested 18 first-party JavaScript and CSS assets across pinterest.com, github.com, web.dev and developer.chrome.com with dcb, dcz in Accept-Encoding, and checked every response for the opt-in header:

$ curl -sD - -o /dev/null \
    -H 'Accept-Encoding: gzip, deflate, br, zstd, dcb, dcz' \
    https://s2.pinimg.com/webapp/10458-6923343ccee71b1e.mjs \
  | grep -iE 'use-as-dictionary|content-encoding|cache-control'
cache-control: max-age=31536000
content-encoding: br

Zero of the eighteen advertised Use-As-Dictionary. That is a small hand-picked sample and not an adoption survey, but it is four sites that are unusually attentive to load performance, serving immutable content-hashed bundles with a one-year max-age - the exact profile the feature was designed for - and none of them had switched it on. Two of those four are published by the browser vendor that shipped it.

Where that leaves the reader

The gap this closes is specific and it has been open since content hashing became standard practice. A hashed filename with a year-long max-age, exactly what Pinterest’s bundles above are served with, guarantees that a cached asset is never revalidated and never stale; it also guarantees that the next build’s asset arrives as a complete stranger. Compression Dictionary Transport is the first mechanism that lets the stranger be paid for with the bytes the browser already has.

What it is worth depends almost entirely on a number your own repository can answer: how much of your bundle changes between the release a returning visitor already has and the one you are about to ship. For two releases of a real 100 KB library four days apart that was 62 bytes of payload. For two a year apart it was 13,945. Both of those are this feature working correctly.

Running the two brotli commands at the top of this article against your last two deploys takes about a minute and tells you which end of that range you are on. That is a better basis for the decision than any of the headline percentages, including the ones quoted here.

Sources

  1. RFC 9842: Compression Dictionary Transport - RFC Editor, IETF, accessed

    Supports: Defines the mechanism. 'Using a previous version of a resource as a dictionary for a newer version enables delivery of a delta-compressed version of the changes, usually resulting in significantly smaller responses than what can be achieved by compression alone' (Section 1.1.1). The 'Available-Dictionary' request header 'is a Structured Field Byte Sequence containing the SHA-256 hash of the contents of a single available dictionary', and 'the client MUST only send a single Available-Dictionary request header with a single hash value for the best available match that it has available' (Section 2.2). Freshness: 'to be considered as a match, the dictionary resource MUST be either fresh or allowed to be served stale' (Section 2.2.1). Matching is same-origin: step 4 of the URL matching algorithm is 'if the Origin of BASEURL and the Origin of URL are not the same, return FALSE' (Section 2.2.2). The 'dcb' encoding has a '36-byte fixed header' of 'Magic_Number: 4 fixed bytes -- 0xff, 0x44, 0x43, 0x42' plus 'SHA_256_Hash: 32 bytes' (Section 4); 'dcz' uses a 40-byte header (Section 5). Transport restriction: 'compression dictionary transport MUST only be used in secure contexts (HTTPS)' (Section 8). Privacy: 'since dictionaries are advertised in future requests using the hash of the content of the dictionary, it is possible to abuse the dictionary to turn it into a tracking cookie', so 'clients MUST treat dictionaries in the same way that they treat cookies', including 'partitioning the storage' and 'clearing the dictionaries whenever cookies are cleared' (Section 10).

  2. Compression Dictionary Transport - MDN Web Docs, Mozilla, accessed

    Supports: Documents both delivery flows and the caching requirement. 'Compression Dictionary Transport can achieve an order of magnitude more compression than compression using a default built-in dictionary.' The server opts in with 'Use-As-Dictionary: match="/js/app.*.js"', and 'when a resource is later requested that matches the given pattern (for example, app.v2.js), the request will include a SHA-256 hash of the available dictionary in the Available-Dictionary header, along with dcb and/or dcz values in the Accept-Encoding header'. An optional 'id' is echoed back by the client in a 'Dictionary-ID' header, but 'the server must still check the hash from the Available-Dictionary header'. A standalone dictionary can be advertised with '<link rel="compression-dictionary" href="/dictionary.dat" />' and 'is then downloaded by the browser during idle time'. Caching: 'if the response is cacheable, it must include a Vary header to prevent caches serving dictionary-compressed resources to clients that don't support them or serving the response compressed with the wrong dictionary', namely 'Vary: accept-encoding, available-dictionary'. Status and limits: 'this feature is not Baseline because it does not work in some of the most widely-used browsers'; 'dictionaries could themselves become tracking vectors so browsers may restrict this feature when cookies are disabled or when other extra privacy protections are enabled'; and 'dictionaries must be same-origin with the resource using the dictionary'.

  3. Improving Google Search with Compression Dictionaries - Chrome for Developers, Google, accessed

    Supports: The one large-scale production deployment with published numbers. Google Search built a dictionary 'from a representative sample of search results' and this 'reduced the average size of the HTML payload across all Chrome users by 23% compared to standard Brotli compression'. The effect on rendering: 'this resulted in a Largest Contentful Paint (LCP) improvement of 1.7% overall, and up to 9% on high latency networks'. Keeping the dictionary current is itself infrastructure: 'a robust automated pipeline ensures the dictionary remains fresh, keeping pace with frequently changing SRP content that is released multiple times a day.'

  4. Shared Dictionaries: compression that keeps up with the agentic web - Cloudflare, accessed

    Supports: Published 17 April 2026, announcing a passthrough beta from 30 April 2026 in which Cloudflare 'forwards the headers and encodings that shared dictionaries require like Use-As-Dictionary, Available-Dictionary'. Their lab measurement on successive near-identical JS bundles: a 272KB asset compressed to 92.2KB with gzip and to '2.6KB. That's a 99% reduction from the uncompressed asset, and still 97% smaller than gzip.' Timing on a cache miss: 'DCZ TTFB is roughly 60 ms faster than gzip', with download completion at '1ms versus 161ms for gzip'. The operational cost, stated plainly: 'responses vary on both encoding and dictionary hash, so every dictionary version creates a separate cache variant...Hit rates drop, storage climbs', and during a deploy 'you have clients with the old dictionary, clients with the new one, and clients with none'. Browser support at publication: 'Chrome 130+ and Edge 130+, with Firefox support in progress'.

  5. npm registry metadata for the vue package - npm, Inc., accessed

    Supports: Source of the publication dates used to convert version distance into elapsed time in the bench. The registry's `time` map records vue 3.4.38 at 2024-08-15, 3.5.17 at 2025-06-18, 3.5.19 at 2025-08-21 and 3.5.20 at 2025-08-25. The compressed files themselves were fetched from the corresponding jsDelivr npm mirror paths, `https://cdn.jsdelivr.net/npm/vue@<version>/dist/vue.runtime.global.prod.js`.