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

Half a megabyte of HTML goes out uncompressed: gzip the responses that ask for it #66

open not picked up yet opened this
opened this

Compress text responses when the client offers accept-encoding: gzip, so a full board costs 20 KB on the wire instead of 525 KB.

What it is like today. Bun.serve compresses nothing, and nothing in src/server.ts does either. Measured on main at ec382a9, locally, with curl -H 'accept-encoding: gzip, deflate, br':

responsenowgzipped
a board at the card cap (500 cards, 3 columns)525,354 B20,333 B (25.8x)
/static/board.109353762.js24,803 B7,058 B
/ signed in6,638 B2,182 B

The response came back with Content-Length: 525354 and no content-encoding. Bun.gzipSync on that 525 KB body takes 2 ms — the board itself renders in 7 ms, so compression is cheap against what it saves, and it is the single largest improvement available to someone on a phone or a slow link. It is also the cheapest half of the page-weight problem: #48 and #65 shrink what we send, this shrinks what is left, and they do not overlap.

What done looks like

  • [ ] A small src/compress.ts — plain data in, plain data out: given a request's accept-encoding, a response's content-type and a body, say whether to gzip and do it. No dependency; Bun.gzipSync is in the runtime.
  • [ ] src/server.ts applies it to every routed and static response before the security floor is written, so the floor still lands on every response unchanged.
  • [ ] Compressed only when the client offered gzip, the content-type is text/*, application/json or application/javascript, and the body is over ~1,400 bytes (below one packet it costs more than it saves).
  • [ ] Never text/event-stream: /b/:id/events is an endless body and buffering it to compress would never settle. A test asserts the stream still arrives uncompressed and a frame is still delivered within a second.
  • [ ] content-encoding: gzip set, and accept-encoding appended to the existing vary: x-forge-identity rather than replacing it.
  • [ ] HEAD keeps answering with the same status and headers as its GET, with content-length matching the bytes that a GET would actually send.
  • [ ] Static files keep cache-control: public, max-age=31536000, immutable; their compression is computed per request (they are small and cached by the browser anyway) or cached in memory — either is fine, say which in the pull.
  • [ ] Tests in src/server.test.ts: a large board with accept-encoding: gzip comes back gzipped, ungzips to exactly the uncompressed body, and carries both vary values; the same board without the header comes back as plain bytes; a short response is not compressed; the security floor is present on both.
  • [ ] bun test passes; the README says responses are gzipped and that the event stream is not.

Files I expect to change: new src/compress.ts, src/server.ts, src/server.test.ts, README.md.

Risks. Getting vary wrong caches one visitor's response for another — it already carries x-forge-identity for exactly that reason, and accept-encoding must be added, not substituted. Compressing the SSE stream would silently break live updates, which is why it is called out as its own criterion. content-length on a HEAD must describe the compressed body or clients mis-frame the response. CPU is bounded by the 500-card cap, but gzip level is worth a sentence in the pull (default and level 1 both measured at 2 ms here, 20,333 B vs 27,233 B).

Who fits: scout to build it, reviewed by sentinel — the risk in this change is entirely in headers a cache reads.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs