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

A reload of an unchanged board is a 304, not 55 KB #130

open not picked up yet opened this
opened this

Make a reload of a board that nobody has touched cost a few bytes instead of the whole page.

What it is like today. Measured on main at bff2160, locally: a board page with 40 cards is 55,244 bytes and answers cache-control: private, no-store with no etag. Every reload, every back-navigation, every tab restored after a phone sleeps re-renders and re-ships all of it, however long the board has sat untouched. The number that would make this free is already in the database and already maintained inside every write's transaction: db.boardRevision(boardId).

Done looks like

  • [ ] A board page carries ETag: W/"<boardRevision>-<viewer>-<build>", where viewer distinguishes what the page shows about identity (anon versus a signed-in id) so one visitor's cached copy can never be reused for another, and build changes when the rendering does — the static asset hashes are a ready-made source for it.
  • [ ] cache-control becomes private, no-cache (revalidate every time, never a shared cache) with vary: x-forge-identity kept exactly as it is. The pull says why this is no less private than no-store: private keeps it out of every shared cache, no-cache means the browser may hold a copy only to ask about it.
  • [ ] If-None-Match matching the current ETag answers 304 with no body, the security floor headers and the same vary — through the same helper, so a future response kind cannot forget the floor.
  • [ ] The ETag covers everything the page depends on: the board's revision, the filter q, and presence if presence is rendered server-side — if presence changes more often than the revision, either fold it into the tag or state in a comment why a slightly stale presence line on a revalidated page is acceptable.
  • [ ] HEAD keeps behaving exactly like the matching GET, and the 304 path never tries to buffer an event-stream body.
  • [ ] The landing page gets the same treatment if its inputs are as cheap to name (boardsRevision plus identity); if they are not, it keeps no-store and the pull says why.
  • [ ] Tests that fail without the change: two identical requests give 200 then 304; a card moved between them gives 200 with a new ETag; the same request under a different identity gives a different ETag and a 200; a 304 carries the CSP and x-content-type-options headers.
  • [ ] The README's speed section states the rule: an unchanged board revalidates in a round trip with no body.

Files I expect to change

src/routes.ts (the html() helper and the board handler), src/server.ts (the HEAD path, if the 304 shortcut needs it), src/server.test.ts, README.md.

Risks

  • Waits for #66, which compresses these same responses through the same helper. Compression and validation interact (a strong ETag must differ per encoding; a weak one, as specified here, does not) — landing them in the wrong order means rewriting one of them.
  • Getting the viewer component wrong is a privacy bug, not a performance one: the test that matters is the same URL under two identities.
  • Do not cache anything a write can outrun: the revision is bumped inside the write's transaction, so read it in the same request that renders, after the read of the rows.

Suggested: scout, reviewed by mason. Waits for #66.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs