Public: anyone can read this; the crew can change it.

forked from trevato/cs

A first visit is 116 kB where 50 kB would do: every response a browser asked to compress is compressed #236

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Pushed. Here's the pull description:

shipped production is serving it deploy ae07c2d

opened this

Pushed. Here's the pull description:

---

Every response worth compressing is now compressed, decided in one place: send in src/server.ts is the single gate where a body meets accept-encoding and the only place a HEAD loses its body, so a HEAD answers with exactly the headers its GET would have, content-encoding and vary included. src/compress.ts (new, no dependency) holds the two rules — a body is worth gzipping when its content type is text-shaped (text/\, JSON, JavaScript, SVG, +json/+xml; a PNG or font is packed already and is never tried), when it is over GZIP_MIN = 1,024 bytes, and when it actually comes out smaller. The 9-byte forbidden would grow* — gzip's frame is 18 bytes — so it is sent as it always was, with no vary, because nothing about it depends on that header.

Measured on this tree, bytes on the wire:

whatbeforeafter
/17,9946,494
/range14,9455,693
/example10,5524,099
static/app.<hash>.js91,22536,694
static/range.<hash>.js34,81014,757
static/example.<hash>.js21,1159,292
static/weights.<hash>.json11,1208,059

A first visit to / — page, bundle, weights — is 51,247 bytes where it was 120,339: 50.0 kB against 117.5 kB on the repository's 1,024-byte ruler. (The issue's 116.8 kB was 0577870 and counted a kB as 1,000; the tree has grown since.)

The cost at boot: static/ is read and gzipped once, before the server binds — 4.0 ms for all four files, 154.6 kB raw and 67.2 kB gzipped held in the process — and the boot log now prints that line, so the cost is observable rather than described. A hit on a bundle is a map lookup and a write of bytes that already exist: 1.5 ms end to end locally, against the 2.4 ms of gzip per request that compressing on the way out would have cost every visitor with a cold cache. The 404 is weighed the same way; a page, which is rendered per request, is weighed with it at 0.38 ms for an 18 kB document. The level is 6, not the 9 scripts/build.ts measures ceilings at: level 9 costs 75% more CPU for 1.5% fewer bytes on the shooter's bundle and is 192 bytes worse on the weights. The ceilings stay at 9 — that number is a ruler between machines, not a claim about the wire.

What I chose not to do: no brotli or zstd and no streaming compression dependency — three games and a leaderboard do not need one, and gzip is what every client offers. The 500 is the one response that does not pass the gate, because Bun hands the error handler an error and no request, so there is no accept-encoding to read. Two small behaviour changes fell out and are deliberate: files in static/ are read at start-up only (a deploy writes them before this process starts, and the map lookup is now also the path check — /static/../src/server.ts is a 404), and a POST to a /static/ path is a 404 rather than the file. #42's gzip half is done here; what is left there is "reads only".

Tests that would fail without this: src/compress.test.ts pins the type rule, the accept-encoding parsing (gzip;q=0, *, a missing header) and the threshold, including that forbidden gzips larger than it is; src/server.test.ts checks for pages, both assets and the 404 that a gzipped response gunzips byte for byte to what an uncompressed one is, that both carry vary and the full security floor and that an asset keeps cache-control: public, max-age=31536000, immutable either way, that HEAD matches GET header for header with gzip asked for, that every file in static/ is held compressed at start-up, and holds /, /range and /example to a byte budget with the measured numbers written down beside it. One existing test changed rather than the code: src/pages/example.test.ts allows the wire level among the numbers the example page may print, because that page now states the level it was actually sent at — before this pull its "N kB of them over the wire" was 4 kB of fiction about a 21 kB download.

To try it: curl -sI --compressed <preview>/ and the same without --compressed, curl -s --compressed <preview>/static/app.<hash>.js | wc -c, and the app's boot log for the line naming what static/ cost to hold.

🤖 Generated with Claude Code

Closes #229

commented

Merged, resolved, green (1,078 tests, typecheck clean) and pushed.

Every response worth compressing is now compressed, decided in one place: send in src/server.ts is the single gate where a body meets accept-encoding and the only place a HEAD loses its body, so a HEAD answers with exactly the headers its GET would have, content-encoding and vary included. src/compress.ts (new, no dependency) holds the two rules: a body is gzipped when its content type is text-shaped (text/\, JSON, JavaScript, SVG, +json/+xml — a PNG or font is packed already and is never tried), when it is over GZIP_MIN = 1,024 bytes, and when it actually comes out smaller. The 9-byte forbidden would grow* — gzip's frame is 18 bytes — so it is left alone and carries no vary.

Measured on the merged tree, bytes on the wire:

whatbeforeafter
/18,6626,758
/range14,9455,693
/example10,7614,180
static/app.<hash>.js95,02938,119
static/range.<hash>.js34,81014,757
static/example.<hash>.js21,1159,292
static/weights.<hash>.json11,1208,059

A first visit to / — page, bundle, weights — is 52,936 bytes where it was 124,811: 51.7 kB against 121.9 kB on the repository's 1,024-byte ruler. static/ is read and gzipped once, before the server binds — 4.5 ms for all four files, 158.3 kB raw and 68.6 kB gzipped held in the process, and the boot log now prints that line — so a hit on a bundle is a map lookup and a write of bytes that already exist (0.7 ms end to end locally) rather than the 2.5 ms of gzip per request that compressing on the way out would cost every visitor with a cold cache. The 404 is weighed the same way; a page, rendered per request, is weighed with it at about 0.4 ms. Level 6, not the 9 the ceilings are measured at: level 9 costs 67% more CPU for 1.5% fewer bytes on the bundle and is 192 bytes worse on the weights.

The merge with #235: /api/plan moved into route with every other path, so it passes the same gate — a plan is 156 bytes, under the threshold, and goes out as it is with cache-control: no-store and no vary; its refusals use the shared text() helper, and both new boot-log clauses are kept. One thing to look at: static/ arrived from main holding two app.*.js bundles, and neither was what main's own sources build, so bun run scripts/build.ts ran here — app.ae4ebe5234.js replaces both, which is also what main's "the bundle in static/ is the one the sources produce" test was already failing on. All the prose numbers were re-measured on the merged tree.

What I chose not to do: no brotli or zstd and no streaming compression dependency. The 500 is the one response that skips the gate, because Bun hands the error handler an error and no request. Two deliberate side effects: files in static/ are read at start-up only (the map lookup is now the path check, so /static/../src/server.ts is a 404), and a POST to a /static/ path is a 404 rather than the file. #42's gzip half is done here; what is left there is "reads only".

Tests that would fail without this: src/compress.test.ts pins the type rule, the accept-encoding parsing (gzip;q=0, *, a missing header) and the threshold, including that forbidden gzips larger than it is; src/server.test.ts checks for pages, both assets and the 404 that a gzipped response gunzips byte for byte to what an uncompressed one is, that both carry vary and the full security floor and that an asset keeps its year-long immutable cache-control, that HEAD matches GET header for header, that static/ is held compressed at start-up, and holds /, /range and /example to a byte budget. One existing test changed rather than the code: src/pages/example.test.ts allows the wire level among the numbers the example page may print, because that page now states the level it was actually sent at — before this pull its "N kB of them over the wire" described a download that went out uncompressed.

To try it: curl -sI --compressed <preview>/ and the same without --compressed, curl -s -H 'accept-encoding: gzip' <preview>/static/app.<hash>.js | wc -c, and the app's boot log for the line naming what static/ cost to hold.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs