# Findings: first visit and later pages (DECISIONS H1)

Measured 2026-09-16 from Boxmeer (NL), Chrome 152, `tools/web_timing.mjs`, 5 runs per
series. Raw series: `2026-09-16-first-visit.jsonl`. FCP medians in ms.
Budget (H1): target 50, limit 100.

## Hosts, same page (`boxdraw-font`, 283 KB)

| Host | First visit | Later page |
|---|---|---|
| Hetzner cx23 Nuremberg + Caddy | 164 | 24 |
| Cloudflare Pages | 220 / 324 | 84 / 92 |
| Bunny CDN, edge `AMS1`, all files cached at the edge | 116–148 | 24 |
| Fly.io `ams`, Caddy, one machine always on | 112 / 112 | 24 |
| Scaleway DEV1-S `nl-ams-1`, Caddy on the VPS itself | 196 / 120 | 24 |

## Page weight, same host (Bunny AMS1), and localhost as the floor

| Page | Bytes | localhost | Bunny |
|---|---|---|---|
| `bare` — HTML only, no CSS, no JS | 124 | 32 | 96 / 112 |
| `q` — RASTER Q: inline CSS, system `monospace, monospace` | 334 | 28 | 92 / 92 |
| `boxdraw-font-subset` — Plex subset to Latin-1 woff2 | 45 550 | 36 | 124 / 116 |
| `boxdraw-font` — full Plex TTFs | 283 335 | 36 | 116 / 120 |

Same four pages on Fly.io `ams` (Caddy in a container, one machine always on, h2):
`bare` 92 / 84, `q` 92 / 132 (one run with a 361 ms first byte), `boxdraw-font-subset`
120 / 112, `boxdraw-font` 112 / 112. Within noise of Bunny: a nearby plain server
matches the CDN edge for a Dutch visitor.

And on a plain Scaleway VPS in Amsterdam (no proxy in front): `bare` 96 / 168, `q`
96 / 100, `boxdraw-font-subset` 124 / 124, `boxdraw-font` 196 / 120. The best runs
(80–104 ms) equal Fly and Bunny; the medians are noisier, with single runs of 330–400 ms
first byte. No proxy removed saves nothing measurable: all three nearby hosts land on the
same ~90 ms floor.

Later pages: 16–28 ms for every page on Bunny and Fly.

## What this means

1. **~30 ms is Chrome's own floor.** On localhost every page, 124 B or 283 KB, paints its
   first frame at 28–36 ms. No page gets below that.
2. **~60 ms is the network, for any page.** Bunny's AMS1 edge from Boxmeer: TCP ≈ 8 ms,
   TLS ≈ 30 ms, HTML at ≈ 70 ms. The localhost column is *no network*, not "no CDN";
   the gap is not something a CDN adds.
3. **A first visit under 50 ms over HTTPS is not reachable.** 30 + ~60 ≈ 90 ms. The
   100 ms limit is: `bare` and `q` meet it; the font pages are at 116–124.
4. **Fonts cost one extra round trip (~20–30 ms), not bytes.** Subsetting 283 → 46 KB
   made no measurable difference on this connection; the number of requests matters more
   than their size. Unmeasured on a slow or mobile link, where size will count.
5. **A CDN is faster only when it caches the page and is nearer.** Bunny beat Nuremberg
   by 30–50 ms because Amsterdam is closer. Cloudflare Pages was slower than Nuremberg: its
   HTML is not cached at the edge (TTFB 100–400 ms) and every `.html` URL costs a redirect.
   What a CDN buys is a server near *every* visitor; for a Dutch audience one server in
   Amsterdam likely does the same with less machinery (not yet measured).
6. **Later pages are solved** at 16–28 ms everywhere that caches; speculation rules
   (prerender on hover, `web/spike/boxdraw-font.html`) should take Chrome toward 0.
7. **Varnish or Redis would not help.** They cache work a server does to build a page;
   these pages are static files and server time was already 1–2 ms.

## Method notes and mistakes

- Network noise is larger than the differences between light pages: the same `bare` page
  scored 160–188 ms in a series taken 5 minutes before the valid one. Interleave variants
  in rounds and repeat before comparing; single ping RTTs swung 11 → 23 ms.
- Two Bunny series were **void**: CSS, JS and subset fonts returned 404 because a broken
  pipe (`| uniq -w`, not on BSD) killed the upload loop. Caught by checking
  `content-type: text/html` on a `.woff2`. Always assert 200 on every asset before timing.
- Results were copied by hand into the JSONL. `web_timing.mjs` should append its own.
- `CacheControlMaxAgeOverride` on Bunny also made HTML cacheable at the edge; that is what
  made it fast and is a choice to make per page (stale HTML after a deploy until purge).

## Not measured

- Vultr Amsterdam (Fly, Scaleway and Bunny already agree on the floor).
- Cloudflare's CDN (not Pages) with HTML caching in front of a server.
- HTTP/3: every run negotiated `h2`.
- Firefox, Safari, mobile networks, visitors outside NL.
