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
Check · Review · Land · Live done — Landed and live in production.
Pushed. Here's the pull description:
shipped production is serving it deploy ae07c2d
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:
| what | before | after |
|---|---|---|
/ | 18,662 | 6,758 |
/range | 14,945 | 5,693 |
/example | 10,761 | 4,180 |
static/app.<hash>.js | 95,029 | 38,119 |
static/range.<hash>.js | 34,810 | 14,757 |
static/example.<hash>.js | 21,115 | 9,292 |
static/weights.<hash>.json | 11,120 | 8,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
Pushed. Here's the pull description:
---
Every response worth compressing is now compressed, decided in one place:
sendinsrc/server.tsis the single gate where a body meetsaccept-encodingand the only place aHEADloses its body, so aHEADanswers with exactly the headers itsGETwould have,content-encodingandvaryincluded.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 overGZIP_MIN= 1,024 bytes, and when it actually comes out smaller. The 9-byteforbiddenwould grow* — gzip's frame is 18 bytes — so it is sent as it always was, with novary, because nothing about it depends on that header.Measured on this tree, bytes on the wire:
//range/examplestatic/app.<hash>.jsstatic/range.<hash>.jsstatic/example.<hash>.jsstatic/weights.<hash>.jsonA 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 was0577870and 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 9scripts/build.tsmeasures 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-encodingto read. Two small behaviour changes fell out and are deliberate: files instatic/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.tsis a 404), and aPOSTto 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.tspins the type rule, theaccept-encodingparsing (gzip;q=0,*, a missing header) and the threshold, including thatforbiddengzips larger than it is;src/server.test.tschecks for pages, both assets and the 404 that a gzipped response gunzips byte for byte to what an uncompressed one is, that both carryvaryand the full security floor and that an asset keepscache-control: public, max-age=31536000, immutableeither way, thatHEADmatchesGETheader for header with gzip asked for, that every file instatic/is held compressed at start-up, and holds/,/rangeand/exampleto a byte budget with the measured numbers written down beside it. One existing test changed rather than the code:src/pages/example.test.tsallows 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 whatstatic/cost to hold.🤖 Generated with Claude Code
Closes #229