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, /around and /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 and wrote the fix, cloudflare/workerd#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 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:
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.
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:
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 is mostly shell and loses 39%, and /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.
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
dictionarythat wasn't a buffer; since nodejs/node#65867, first released in v26.10.0, they throwERR_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
pledgedSrcSizewhen 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 --trainmakes, writes its ID into every frame, so a reader without it fails cleanly asDictionary 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 and /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, #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.