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

The stylesheet is not part of every page: one cached CSS file, and a policy without 'unsafe-inline' #127

open not picked up yet opened this
opened this

Take the stylesheet out of every page and out of the security policy: one content-named CSS file, cached for a year, and style-src 'self' with no 'unsafe-inline'.

What it is like today. Measured on main at bff2160, locally: the styles template literal in src/pages/board.ts is 5,947 bytes, and shell() inlines it in a <style> block on every response. That is 78% of the landing page (7,624 bytes) and 48% of an empty board page (12,460 bytes), re-sent on every navigation, and it is why the security floor in src/server.ts has to say style-src 'self' 'unsafe-inline' — the one allowance that makes injected markup able to restyle the page (cover the board with a full-screen element, paint a fake sign-in over it) if anything ever escapes escape(). The app already serves content-named files from static/ with a year-long cache; the stylesheet is the one asset that does not use it.

Done looks like

  • [ ] The styles move to static/app.<hash>.css, named by content like board.<hash>.js, served by the existing /static/ path with cache-control: public, max-age=31536000, immutable, and referenced from shell() as a single <link rel="stylesheet">.
  • [ ] style-src 'self' — 'unsafe-inline' is gone from the floor in src/server.ts.
  • [ ] No page carries a <style> block or a style="..." attribute any more. The few dynamic bits that need one (if any remain) become classes; the pull lists them.
  • [ ] No flash of the wrong theme: static/theme.*.js still runs before the stylesheet is applied and still sets data-theme on <html> — verify in a browser and say so in the pull, since this is the one thing the inline block was protecting.
  • [ ] First paint is measured before and after (cold, one visitor): the extra round trip for a cold cache against ~6 KB saved on every warm navigation, stated honestly in the pull, with the numbers.
  • [ ] Tests that fail without the change: the CSP header on an HTML response has no 'unsafe-inline'; a rendered board page contains no <style> and no style=; /static/app.<hash>.css answers 200 with the immutable cache header and content-type: text/css.
  • [ ] The README's speed section says the stylesheet is cached like the scripts.

Files I expect to change

src/pages/board.ts (the styles literal leaves, a STYLES_CSS constant arrives next to BOARD_JS), static/app.<hash>.css (new), src/server.ts (the floor), src/server.test.ts and src/pages/board.test.ts, README.md.

Risks

  • Waits for #80, which asserts the floor on every kind of response — it is the same header and the same tests, and two runs editing that assertion set at once is a needless conflict.
  • A render-blocking <link> on a cold cache is one extra request before first paint. That is the trade; measure it rather than asserting it, and keep the file small enough that it lands in the same round trip as the HTML on a warm connection.
  • Do not replace the allowance with a nonce: a nonce means per-response CSP and inline CSS forever. One file is simpler and cacheable.

Suggested: scout, reviewed by sentinel. Waits for #80.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs