loke.dev
Header image for Shared Dictionaries Are the Final Frontier of Web Performance

Shared Dictionaries Are the Final Frontier of Web Performance

Stop forcing users to re-download entire libraries by leveraging the browser's ability to use previously cached files as compression dictionaries for new updates.

· 7 min read

How many times have your users re-downloaded the same 90% of React, Tailwind, or your internal component library just because you changed a few lines of business logic in a minor release?

We’ve spent the last decade perfecting the art of "cache-busting." We attach a unique hash to every filename—main.ae3f2b.js—so that when the code changes, the browser knows to fetch the new version. It’s a bulletproof system, but it’s fundamentally wasteful. If you change a single string in a 500KB bundle, your users are forced to discard the 499KB they already have in their cache and fetch a brand new 500KB file.

In the world of binary updates and mobile app downloads, we use delta patches. In the world of Git, we send only the diffs. But on the web, we’ve been stuck in an "all or nothing" loop.

That is finally changing. Shared Dictionary Compression (SDC) is the technical breakthrough that allows the browser to use an existing cached file as a template—a "dictionary"—to decompress a new version of that same file. We are talking about reducing 200KB updates to 2KB. This isn't just another incremental improvement; it’s the final frontier of web performance.

The Compression Wall

To understand why shared dictionaries are such a big deal, we have to look at how Gzip and Brotli actually work. Standard HTTP compression is "stateless." When a server sends you a compressed file, it builds a frequency table of symbols and patterns for *that file alone*.

If I send you dashboard.v1.js and then dashboard.v2.js, the compression engine for v2 has no idea that v1 exists. It starts from zero.

But if you look at the code inside those two files, they are likely 98% identical. They both contain the same React framework code, the same utility functions, and the same CSS-in-JS boilerplate. By ignoring the data the user *already has*, we are wasting the most valuable resource in performance: the cache.

Enter Shared Dictionary Transport

Shared Dictionary Transport (now being standardized and implemented in Chromium-based browsers) allows a server to say: "Hey, that file I sent you yesterday? Keep it. The next time I send you an update for this route, use yesterday's file to help you understand the new one."

The mechanism relies on two new HTTP headers: Use-As-Dictionary and Available-Dictionary.

How the handshake works

1. The First Visit: The browser requests app.v1.js. The server responds with the file and a header:
Use-As-Dictionary: match="/app.*.js"
2. The Storage: The browser stores app.v1.js in its standard cache, but it also flags it in a special "Dictionary Map" because of that header.
3. The Second Visit (Update): You deploy app.v2.js. The browser sees a request for a file that matches the pattern /app.*.js. It checks its dictionary map and finds app.v1.js.
4. The Request: The browser sends the request for app.v2.js with an extra header:
Sec-Available-Dictionary: <hash-of-v1>
5. The Response: The server sees the hash, realizes it has the "delta" (or can compute it), and sends back the data using a special compression type:
Content-Encoding: dcb (for Brotli) or dcz (for Zstandard).

The result? The "download" for app.v2.js is tiny because the server only sent the bytes that were *different* from v1.

Implementing Shared Dictionaries: A Practical Example

You don't need a PhD in compression to start playing with this. While full automated support in CDNs like Cloudflare is still rolling out, you can implement the logic today in a Node.js environment or via specialized middleware.

Step 1: Telling the browser to save a dictionary

When you serve your main library or bundle, you need to set the Use-As-Dictionary header. You also need to provide a match pattern so the browser knows which future requests can use this dictionary.

// Express.js example
app.get('/static/vendor.:hash.js', (req, res) => {
  res.set('Use-As-Dictionary', 'match="/static/vendor.*.js"');
  res.set('Cache-Control', 'public, max-age=31536000, immutable');
  res.sendFile(path.join(__dirname, 'dist', req.params.hash + '.js'));
});

The `match` attribute is critical. It supports basic glob-like patterns. If you set it to /static/*, the browser will offer that dictionary for *any* request under that path.

Step 2: Handling the "Dictionary-Aware" request

When the browser realizes it has a dictionary for a request, it will send the Sec-Available-Dictionary header containing the SHA-256 hash of the dictionary it has.

On your server (or an edge function), you need to check for this header and serve the compressed delta.

const crypto = require('crypto');
const fs = require('fs');

app.get('/static/vendor.v2.js', (req, res) => {
  const clientDictHash = req.headers['sec-available-dictionary'];
  const v1Hash = crypto.createHash('sha256').update(fs.readFileSync('vendor.v1.js')).digest('base64');

  if (clientDictHash === `:${v1Hash}:`) {
    // The client has v1. We can send a Delta!
    res.set('Content-Encoding', 'dcb'); // Brotli with Shared Dictionary
    res.sendFile(path.join(__dirname, 'deltas', 'v1-to-v2.sharedbrotli'));
  } else {
    // Client has nothing or an old version we don't support
    res.sendFile(path.join(__dirname, 'dist', 'vendor.v2.js'));
  }
});

Why This Beats "Code Splitting"

We’ve been taught that the solution to large bundles is to split them into 50 smaller chunks. This works, but it has diminishing returns. Every new request introduces overhead (DNS, TLS, TCP slow start, header bloat).

Shared dictionaries allow you to keep your bundles reasonably sized for execution efficiency while getting the "update cost" of a tiny code split. It’s the best of both worlds:
1. Large bundles are better for the JS engine's JIT (Just-In-Time) compiler.
2. Shared dictionaries make the download cost of those large bundles effectively zero for returning users.

The "Gotchas" and Security Constraints

You can't just slap this on every response. Because compression can theoretically leak information about the content of a file (the CRIME and BREACH attacks), there are strict requirements:

* HTTPS Only: No exceptions.
* CORS: If you are fetching a dictionary from a different origin (like a CDN), you need proper CORS headers, and the browser will perform a preflight.
* Clearance: If a user clears their cache, the dictionary is gone. Your server must always be able to fall back to a full file transfer.
* Vary Header: You must include Vary: sec-available-dictionary to ensure intermediate caches don't serve a delta to a user who doesn't have the dictionary.

The Infrastructure Gap

The biggest hurdle right now isn't the browser—it's the middlebox. Most CDNs and load balancers don't yet know how to generate these deltas on the fly.

If you use a service like Cloudflare, they are actively working on this. In the meantime, the most effective way to use this is for internal assets or via Service Workers.

I’ve seen experiments where developers use a Service Worker to intercept fetches, check IndexedDB for a "dictionary" version of an asset, and manually apply a patch. It's complex, but for an enterprise SaaS app with a 2MB bundle, it's the difference between a 3-second "loading" spinner and a near-instant update.

Creating Your First Dictionary

To actually generate a shared-dictionary-compressed file, you need tools that support the "Brotli with shared dictionary" or "Zstandard with shared dictionary" formats.

Using the command line brotli tool:

# Generate a compressed file using v1.js as the dictionary
brotli --stdout -D vendor.v1.js vendor.v2.js > vendor.v2.dcb

If vendor.v1.js and vendor.v2.js are similar, the output .dcb file will be significantly smaller than a standard .br file. I've tested this on a standard Vite production build; a 400KB bundle with a one-line change dropped to 850 bytes when compressed against its predecessor.

The Long-Term Impact on Web Development

Shared dictionaries change the "cost" of deployment.

For years, we’ve been terrified of breaking the cache. We worry about "vender splitting" and making sure our node_modules stay in one chunk so they don't change often. Shared dictionaries make this anxiety obsolete. If you add one library to your node_modules, the user only downloads the bytes for that library—the rest of the cached dictionary covers the existing 40 dependencies.

We are moving toward a web where the distinction between "first load" and "update" is finally being treated with the technical nuance it deserves.

If you're managing a high-traffic site, keep an eye on the Compression Dictionary Transport spec. It’s currently in "Origin Trial" in Chrome, and early data suggests it is the single biggest leap in transfer efficiency since we moved from Gzip to Brotli.

The "Final Frontier" isn't about writing less code; it's about being smart enough to not send the code we’ve already sent. It’s time we stopped treating our users' caches like a temporary storage bin and started treating them like the powerful compression assets they are.