---
title: "aadhar.sh/garage/dictionary: The dictionary the runtime dropped"
description: "13 pages here are written per request and ship as plain brotli, because workerd's zstd took a dictionary option and dropped it. What the fix would save, what it costs, and what it can't reach."
path: "/garage/dictionary"
section: "garage"
kind: "content"
updated: "2026-09-25"
source: "https://aadhar.sh/garage/dictionary"
---

> Site index: https://aadhar.sh/llms.txt
> Section index: https://aadhar.sh/garage/llms.txt
> This is the Markdown twin of a page on aadhar.sh. The HTML at the source
> URL below is the original, and is hand-written and unminified on purpose.

# The dictionary the runtime dropped

Most pages on this site reach a returning Chromium visitor as a delta. The build compresses each one against a 64 KB dictionary made from the site's own markup, and a browser that already holds that dictionary downloads only what differs. Measured against production on 2026-09-25, 51 of the 66 pages here answer that way, and 2 more join them with the release rolling out as I write this.

The other 13 answer as plain brotli. They're the pages the Worker writes when you ask for them, like [/ledger](https://aadhar.sh/ledger), [/around](https://aadhar.sh/around) and [/finger](https://aadhar.sh/finger), so no build ever sees their bytes.

The Worker could compute the delta itself, since `node:zlib`'s `zstdCompressSync` takes a `dictionary` option. In workerd it accepted that option and threw it away.

I filed that as [cloudflare/workerd#6967](https://github.com/cloudflare/workerd/issues/6967) and wrote the fix, [cloudflare/workerd#7106](https://github.com/cloudflare/workerd/pull/7106), which is waiting on review. This page is what the fix is worth to this site, what it costs, and the part it can't reach. [/garage/compression](https://aadhar.sh/garage/compression) covers the build-time half.

## This page, twice

This page is one of the delta pages. Your browser downloaded it once already, as the document you're reading, and the script below asks for it again with `fetch()`.

A fetch has the destination `empty`, and the site offers its dictionary only for requests whose destination is `document`, so the second copy arrives without the delta. Same bytes, two transfers:

enable JavaScript to run the measurement

A page the Worker writes gets the second number both times. That gap, paid on every visit to those 13 pages, is what the fix is for.

## Why nothing complained

A dropped dictionary fails silently on the compressing side, and zstd's design is why. A frame compressed without a dictionary is still a valid frame, and it decodes fine when the reader supplies one anyway. So the only symptom is a delta the same size as no delta at all.

The site's nightly canary runs a four-line control for exactly this. It compresses one 21,200-byte buffer three ways: with no dictionary, with the right one, and with a wrong one.

runtimenonerightwrong

Node 26.9.073 B24 B73 B

workerd, as pinned here73 B73 B73 B

A runtime that honours the option prints a smaller number for the right dictionary alone. workerd prints the same number three times. Decompressing is louder: a frame that needs a dictionary fails there as `Data corruption detected`, against bytes that are fine.

The cause was plumbing. The options cross from JavaScript into C++ through a struct that lists the fields it reads. `dictionary` wasn't one of them, so the value vanished at the language boundary before zstd ever saw it. The fix adds the field and hands the bytes to `ZSTD_CCtx_loadDictionary` and `ZSTD_DCtx_loadDictionary`, the way Node does.

## What it would save

For each of the 13 pages, `tools/runtime-dcz-probe.ts` compressed the live HTML at zstd level 6 against the 64 KB dictionary production serves. It then compared each result with the brotli the edge sends today.

Together they drop from 81,228 to 63,449 bytes, 21.9% off. Each bar is today's brotli, and the green end is the part a runtime delta removes:

[/cache](https://aadhar.sh/cache)10,333 B3,162 → 1,938 B, 1,224 saved (39%)

[/encode](https://aadhar.sh/encode)10,408 B3,231 → 2,028 B, 1,203 saved (37%)

[/finger](https://aadhar.sh/finger)10,637 B3,111 → 1,914 B, 1,197 saved (38%)

[/radar](https://aadhar.sh/radar)10,669 B3,280 → 2,086 B, 1,194 saved (36%)

[/dict](https://aadhar.sh/dict)11,194 B3,546 → 2,278 B, 1,268 saved (36%)

[/agent-ready](https://aadhar.sh/agent-ready)11,625 B3,611 → 2,381 B, 1,230 saved (34%)

[/inbox](https://aadhar.sh/inbox)14,446 B4,316 → 2,943 B, 1,373 saved (32%)

[/lens/census](https://aadhar.sh/lens/census)16,395 B4,625 → 3,155 B, 1,470 saved (32%)

[/ledger](https://aadhar.sh/ledger)20,132 B5,541 → 3,910 B, 1,631 saved (29%)

[/around](https://aadhar.sh/around)23,996 B5,974 → 4,505 B, 1,469 saved (25%)

[/coffee](https://aadhar.sh/coffee)33,539 B7,711 → 6,533 B, 1,178 saved (15%)

[/serendipity](https://aadhar.sh/serendipity)52,539 B15,276 → 13,606 B, 1,670 saved (11%)

[/reading](https://aadhar.sh/reading)63,199 B17,844 → 16,172 B, 1,672 saved (9%)

The percentages spread from 9% to 39%, and the bytes saved barely move. Every page sheds between 1,178 and 1,672 bytes, a median of 1,268, across a sixfold range in page size.

That constant is the shell. The dictionary is built from this site's own pages, so what it matches is the markup they share: the head, the window frame, the tags that load the shell scripts. Every page carries about the same amount of it.

A utility page like [/cache](https://aadhar.sh/cache) is mostly shell and loses 39%, and [/reading](https://aadhar.sh/reading) is mostly its own list and loses 9%.

## What it would cost

A build-time delta costs nothing when a visitor arrives. A runtime delta costs CPU on every request, and this site runs on Workers Free, where a request gets about 10 ms of it.

level 368,167 B16.1% offmedian 0.239 ms, max 0.475 ms

level 663,449 B21.9% offmedian 0.165 ms, max 0.503 ms

level 1960,237 B25.8% offmedian 3.451 ms, max 9.904 ms

Those times are Node 26.9 on an M3 Max, calling the same zstd library workerd wraps, because workerd itself can't run the dictionary call until the fix ships. At level 6 the median page took 0.165 ms and the slowest 0.503 ms. Level 19 buys 3.9 more points and spends up to 9.904 ms on /reading, which is the whole allowance, so level 6 is where the curve bends.

More than half of each level-6 call goes to the dictionary. Without it, the same call takes a median 0.072 ms. `node:zlib` has no way to keep a prepared dictionary between calls, so a Worker would load all 64 KB into a fresh context on every request.

## What it can't reach

The homepage fetches two fragments after it loads, the photo grid and the now-playing list. Both are `fetch()` requests, so the dictionary never answers them, for the same reason the demo's second copy arrives whole.

Forced to use it anyway on 2026-09-15, the grid came out at 1,605 bytes at level 6, against the 1,588 bytes of brotli it already ships. A fragment is mostly unique content, with little shell for a dictionary to match.

The fix makes the delta possible and changes nothing here by itself. It still needs a review and a workerd release. The site's wrangler pin picks up a release within a day, and then the canary's control stops printing the same number three times.

After that the Worker needs the dictionary bytes on hand, an ETag per encoding, `Vary` on `Available-Dictionary`, and a plain response for any dictionary it doesn't recognise.

## What testing the fix turned up

The PR's first coverage report said 7% of its changed lines ran, which was mostly an artifact: Codecov counted the test file itself as untested code. Closing the real gap took four more tests and turned up three things.

- Node changed the rules while the PR was open. Its zstd functions used to ignore a `dictionary` that wasn't a buffer; since [nodejs/node#65867](https://github.com/nodejs/node/pull/65867), first released in v26.10.0, they throw `ERR_INVALID_ARG_TYPE`. The PR throws too now, and its test passes under v26.10.0 and fails under v26.9.0.
- workerd never checks a wrong `pledgedSrcSize` when it compresses in one call. zstd replaces the promised size with the real one whenever all the input arrives at once. Node catches the mismatch by counting the input itself, and workerd needs the same fix, in a PR of its own.
- A trained dictionary, the kind `zstd --train` makes, writes its ID into every frame, so a reader without it fails cleanly as `Dictionary mismatch`. This site's dictionaries are raw bytes with no ID, so the frame can't name them. The dcz header's SHA-256 is what tells the browser which dictionary a delta needs.

## The road the site took anyway

While this page was being written, the site moved two of the original 15 off the Worker's per-request path. [/whoareyou](https://aadhar.sh/whoareyou) and [/garage/dyno](https://aadhar.sh/garage/dyno) now bake their frame at build time, so it ships as a delta today, and fetch the parts that change as a fragment ([#924](https://github.com/oddharsh/site/pull/924), [#926](https://github.com/oddharsh/site/pull/926)).

That road needs no workerd release and no CPU per request. It costs a second request and a placeholder that can't shift the layout, and the data arrives as a fragment, which the dictionary can't reach. On /garage/dyno the first visit came out even: 8,158 bytes across two requests, against 8,060 bytes in one.

Most of the remaining 13 could take the same treatment, one page at a time. The runtime fix is the version that needs no second request, no placeholder and no per-page work, which is why it's worth finishing with the other road open.

## Why bother for 1.3 KB

The objection writes itself: 1.3 KB is a rounding error next to a page of photos. For any single request that's fair.

What changes the arithmetic is the dictionary itself. A returning visitor already paid for it, 14,754 bytes the first time, and today it earns nothing on the 13 pages the Worker writes. The fix lets one download work on every page the site serves, whoever wrote it.

watching · the understanding check is part of the page, not a gate

Source: https://aadhar.sh/garage/dictionary
