Skip to content

perf(vendor): let the browser decompress x2t.wasm - #222

Merged
chaxus merged 1 commit into
mainfrom
perf/wasm-content-encoding
Sep 12, 2026
Merged

chaxus merged 1 commit into
mainfrom
perf/wasm-content-encoding

Conversation

@chaxus

@chaxus chaxus commented Sep 12, 2026

Copy link
Copy Markdown
Member

What

The x2t module ships brotli-compressed as x2t.wasm.br, and every host declares Content-Encoding: br for it. The browser decodes it at the network layer, so instantiateStreaming compiles the module straight off the response and nothing in our code decompresses anything.

before after
on the wire 9,483,006 B (zopfli gzip) 6,898,179 B (brotli -q 11)
decompressed by DecompressionStream('gzip') in JS the browser, at the network layer
loader fetch → sniff magic bytes → rebuild stream → inflate → instantiate fetch → instantiate

2.58 MB off the largest single download in the app, and ~100 lines gone from the two loaders that had to stay in step with each other (x2t_helper.js and packages/converter): sniffAndRebuild in both, the gzip branch of prepareWasmBinary in both, and the DecompressionStream capability check.

Idea taken from a third-party local-first Office site (see #219), verified against their production deployment before starting.

The part that took the time

wrangler pages dev — what e2e-pages runs — re-compresses responses and overwrites Content-Encoding with its own result. A pre-encoded asset then arrives double-encoded, and no browser API can decode brotli to recover. The suite went red on ran: false; x2t never instantiated.

name              Accept-Encoding   response
x2t.wasm          br                Content-Encoding: br,   body = br(br(wasm))
x2t.wasm          (none)            Content-Encoding: br,   body = br(br(wasm))
x2t.wasm          identity          no Content-Encoding,    body = br(wasm)      <- ours dropped, not merged
x2t.wasm.br + CT  br                Content-Encoding: gzip, body = gzip(br(wasm))
x2t-probe.bin     br                Content-Encoding: br,   body = br(wasm)      ✓

--compatibility-flags brotli_content_encoding makes no difference. What wrangler does respect is its own compressibility rule: it leaves application/octet-stream alone, and that is what an unrecognised extension gets.

So two things are load-bearing and neither looks it: the .br extension, and the absence of a Content-Type rule for that path in _headers. Adding Content-Type: application/wasm there is enough to make wrangler compress it again (row 4 — that is how this was first written). Both are documented in _headers next to the rule. The loader labels the response application/wasm itself, so nothing depends on the host typing it.

Verified

environment how result
vite preview (test:e2e) vite.config.ts middleware full 160-spec suite green
wrangler pages dev (e2e-pages) public/_headers green after the rename
static-web-server (e2e-docker) sws.toml twin rule passes through untouched, header preserved
real Cloudflare Pages public/_headers preview-smoke on this PR

What stays

The instantiateWasm hook — no longer decompressing, but still carrying the fetch retry (the CF 500 of 2026-08-20) and the two-way failure reporting the open-failure guard needs. The buffered fallback stays too, now four lines.

After a vendor upgrade

brotli -q 11 -c x2t.wasm > x2t.wasm.br

vendor-contract pins the sha256 of the decompressed module (unchanged by this migration — 7db02f5c…, which is how we know the recompression is lossless) and bounds the compressed size.

Reverse validation (convention 5): removing the Content-Encoding line from _headers turns the new hosting-contract case red.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MKNHFQ1kek1cHeqhbH37Pk

The module ships brotli-compressed as x2t.wasm.br and every host declares
Content-Encoding: br for it, so the browser decodes it at the network
layer and instantiateStreaming compiles it straight off the response.
6,898,179 bytes on the wire against 9,483,006 for the best gzip we could
produce -- 2.58 MB off the largest single download in the app -- and
nothing in our code decompresses anything any more: sniffAndRebuild and
the gzip branch of prepareWasmBinary are gone from both loaders, and
canStreamWasm no longer asks for DecompressionStream.

The .br extension and the absence of a Content-Type rule in _headers are
both load-bearing. wrangler pages dev, which the e2e-pages job runs,
re-compresses anything it considers compressible and overwrites
Content-Encoding with its own result; a pre-encoded asset then arrives
double-encoded and no browser API can decode brotli to recover. It leaves
application/octet-stream alone, which is what an unrecognised extension
gets. Name the file .wasm, or declare a Content-Type for it, and the
editor stops loading under that job. The loader labels the response
application/wasm itself, so nothing depends on the host typing it.

Verified in all three local environments: vite preview (full 160-spec
suite), wrangler pages dev, and static-web-server with sws.toml. Real
Cloudflare Pages is covered by the preview-smoke job.

Reverse-validated: removing the Content-Encoding line from _headers turns
the new hosting-contract case red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKNHFQ1kek1cHeqhbH37Pk
@chaxus
chaxus enabled auto-merge (rebase) September 12, 2026 06:43
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying document with  Cloudflare Pages  Cloudflare Pages

Latest commit: 757a129
Status: ✅  Deploy successful!
Preview URL: https://15888a8c.document-7hm.pages.dev
Branch Preview URL: https://perf-wasm-content-encoding.document-7hm.pages.dev

View logs

@chaxus
chaxus merged commit 9a5b2ca into main Sep 12, 2026
31 of 33 checks passed
@chaxus
chaxus deleted the perf/wasm-content-encoding branch September 12, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant