From abed3f86fa7226eec3ca5b979cfdc4a72164be79 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 20 Aug 2026 06:57:22 +0200 Subject: [PATCH] perf: preload the feed from the shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `fetchFeed` cannot start until main.js has downloaded, parsed and mounted React, so the one request this page exists to make was the last one to leave. Nothing about the URL depends on the bundle, so the preload scanner can have it in flight in the first few milliseconds instead. Measured on a 4G/4x-CPU profile with a cold cache and gzip on: the events reach the screen at ~470ms instead of ~570ms, and the loading state stops being something a reader can see rather than merely being brief. First contentful paint is unchanged within noise — the win is that the feed is off the critical path entirely, arriving at 216ms while the bundle is still downloading. `crossorigin` on that link is not decoration. `as="fetch"` has to match the credentials mode of the `fetch()` that follows, and a mismatch is not a no-op: the browser throws the preload away and downloads the feed twice, which is worse than not preloading at all. Verified by counting requests in a real browser, because no test here can see it. Co-Authored-By: Claude Opus 5 (1M context) --- docs/ARCHITECTURE.md | 27 +++++++++++++++++++++++++++ index.html | 18 ++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 00998a9..61e5732 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -240,6 +240,33 @@ back to the last copy seen). Countdowns run off the device clock, so the app sta network. Offline state is surfaced in the header and above the footer — stale data must never be presented as current. +### First load + +The critical path on a cold visit is short and worth keeping that way: `index.html` → `styles.css` + +`main.js` in parallel → React mounts → the feed renders. Two things about it are deliberate. + +**The bundle is built with `--production`** (`package.json` → `build:js`). Without that flag Bun +ships React's development build, which is 222 KB of extra JavaScript, validates every element +creation at runtime, and leaves `StrictMode` double-invoking effects — so `fetchFeed` runs twice and +every reader downloads the feed twice. It shipped that way until 2026-08-20. See AGENTS.md § Commands +for the trap in the obvious-looking alternative, which builds cleanly and does not run. + +**The feed is preloaded from the shell.** `fetchFeed` cannot start until `main.js` has downloaded, +parsed and mounted, so the one request the page exists to make was starting last — measured on a +4G/4×-CPU profile, the bundle finished at ~400 ms and the feed then ran to ~570 ms. Nothing about +that URL depends on the bundle, so `` puts it on the wire in the first +few milliseconds and it is already in cache when React asks: the events are on screen at ~470 ms +instead, and the loading state is gone rather than merely brief. + +The `crossorigin` attribute on that link is load-bearing. `as="fetch"` must match the credentials +mode of the `fetch()` that follows it, and a mismatch is not a no-op — the browser discards the +preload and fetches the feed a second time, which is worse than not preloading. If that link is ever +edited, count the feed requests in a real browser; nothing in the test suite can see this. + +Compression is the server's job and both hosts do it: GitHub Pages transparently, and `serve.ts` — +which is what the Docker image runs — by negotiating `accept-encoding` per request. It matters more +than it sounds: the bundle is 344 KB raw and 100 KB gzipped. + ### The theme, before the bundle arrives The page is drawn dark by default and light when the reader has asked for it (PRD F15). Which one diff --git a/index.html b/index.html index 85e42ca..4ba08af 100644 --- a/index.html +++ b/index.html @@ -62,6 +62,24 @@ } })(); + +