Loading & Runtime Audit — 2026-08-11¶
Triggered by: “full website loading runtime audit — everything loads quickly, fast, using low amounts of data bandwidth; revamp the database layout or how API calls are handled if needed.” Then: “ensure everything is fixed.”
This folder is the report. It is written to be read by an agent picking up the work, so every claim carries the command that produced it and every recommendation carries the file it lands in.
How to use this folder (read this first if you are an LLM)¶
Read this page, then
06-backlog.md. Those two are enough to pick the next task. The numbered files are evidence, not prerequisites.Numbers here came from a real build and a real browser, not from reading source. Where a claim is inferred, it says so.
Prefer build-level numbers over browser-level numbers when they disagree. A chunk graph is deterministic; an FCP in a shared container is not.
01-measurements.mdmarks which is which.Read “Corrections” below before acting on any earlier audit — or on the first draft of this one. Five separate claims turned out to be wrong under measurement, including two of mine. Every one of them would have sent you to the wrong file.
“Fixed” means in this branch and verified. Don’t re-diagnose those.
Where the time and bytes actually go¶
For / on a cold anonymous load:
Layer |
Cost |
Verdict |
|---|---|---|
Origin HTML |
26–80 ms warm TTFB |
Not the problem. Fast, and now cacheable. |
Database |
Well-indexed, batched, SWR-cached |
Not the problem. |
JavaScript |
~380 requests, ~1.1 MB encoded |
This is the problem. |
CSS |
465 KB raw / 48 KB brotli, one sheet |
Secondary, real, untouched. |
Images |
~0 raster bytes on |
Not the problem — and not what earlier audits thought. |
API responses |
269 GET handlers; caching now on the ones that can take it |
Improved; the rest is per-route judgement. |
The request count is the headline, not the byte total. 380 requests to
render a homepage is a number no byte budget rescues: on a phone on 4G,
per-request overhead dominates long before the payload does. And the one lever
three audits assumed would fix it (codeSplitting.minSize) was measured this
pass and makes it worse — see 06-backlog.md §5.
Neither the database nor the API handler layer needed the revamp the brief authorised. Both are well-built. What the API layer needed was adoption of the caching it already implements, and that is now done for every endpoint that can safely take it.
Fixed in this branch¶
Client-side bytes and requests:
Fix |
Measured effect |
Detail |
|---|---|---|
The 431 KB lucide icon chunk, at its root cause — 4 files did |
|
|
framer-motion’s layout projection off the critical path — 9 modules imported the full |
|
|
socket.io-client off every page — |
|
|
zod out of 9 route modules (catalog + 7 rmhladder + 2 hand-rolled parsers) |
Removed from those route chunks. Still on the critical path via 246 other paths — see Corrections. |
|
An eslint rule so the icon chunk cannot come back |
|
|
Bandwidth and caching:
Fix |
Measured effect |
Detail |
|---|---|---|
Cache policies on all 13 GET endpoints that could safely take one |
|
|
AVIF in the image pipeline + |
2.93 MB AVIF vs 4.38 MB WebP for the same art — 33% smaller |
|
A 928 KB PNG served at full size into a Discord PIP tile |
Now goes through the variant pipeline that already had 320/640/960 for it |
|
Cloudflare cache rules applied — and two bugs found in the live zone |
A static-asset rule was governing |
|
Server and correctness:
Fix |
Measured effect |
Detail |
|---|---|---|
9 routes no longer block first paint on |
Render-blocking third-party font references: 9 → 0 |
|
|
Header absent → |
|
A missing |
Reproduced, then fixed; 2 sibling AI clients had it too |
|
Tests, because four of the above were untested machinery:
New test file |
What it pins |
Tests |
|---|---|---|
|
|
16 |
|
Every |
10 |
|
|
6 |
|
Manifest integrity; every width exists in both formats |
7 |
Suite: 274 files / 6,778 tests green (was 271 / 6,739).
Aggregate, build-level and deterministic:
Metric |
2026-08-04 |
2026-08-09 |
before |
after |
change |
|---|---|---|---|---|---|
Entry chunk |
253.6 KB |
273.9 KB |
277.2 KB |
276.0 KB |
−0.4% |
Critical path, raw |
1028.5 KB |
1219.2 KB |
1323.3 KB |
1182.1 KB |
−10.7% |
Critical path, brotli |
297.6 KB |
313.8 KB |
380.2 KB |
338.1 KB |
−11.1% |
Critical-path chunks |
94 |
116 |
131 |
108 |
−23 |
Monolithic icon chunk |
433 KB |
433 KB |
431.3 KB |
gone |
— |
Requests to render |
— |
248 |
533 |
382 |
−28% |
Long tasks on |
— |
337 ms |
813 ms |
~570 ms |
−30% |
Compare the 08-04/08-09 columns for direction only (different harnesses, different hardware); the last two columns are the same harness on the same machine.
Corrections¶
Five claims that were wrong under measurement. Two are from the first draft of this very audit — recorded so the next agent trusts the measurement, not the prose.
From ../performance-audit-2026-08-09.md:
The icon chunk was not “552 files each importing a few icons”. It was four files doing a computed lookup on a namespace import, which is unshakeable. That audit recommended a per-icon codemod across ~588 files (OPT-10); it was unnecessary. Do not do it. A 4-file fix plus a lint rule deleted the chunk from the build.
/gamesdoes not download 2.07 MB of card art. It downloads no raster art at all — the cards are a CSS gradient plus a lucide icon, and the SSR HTML for/gamescontains zero<img>tags.
From the first pass of this audit (now fixed in this folder):
“
/gamesdownloads 296 KB of images” was my own harness miscounting. Those were inlinedata:image/png;base64,…URIs, which match an image extension regex but are not network transfers. Real answer is #2 above.“26 page routes import zod for
validateSearch” was wrong twice over. Nine route modules imported zod, and all but one used it forcreateServerFnvalidators, notvalidateSearch. Fixing all nine did not remove zod from the critical path, because 246 client-reachable modules import it — it is woven through the shared schema layer (lib/feed/signals.ts,lib/rmhark-schema.ts,lib/emoji/packs.ts, …), not concentrated in routes. See06-backlog.md§1.“Wave 2: add
etag: trueto the 100 authenticated GETs” would have bought almost nothing. A browser only sendsIf-None-Matchfor a response it stored, and it will not store a response with noCache-Control. An ETag without a freshness policy is inert for browser clients; it pays off for API clients that persist etags themselves. Corrected guidance in03-api-caching.md§4.
One thing 08-09 got right and this pass confirms: the anon-HTML cache fix works.
cache-control: public, max-age=0, s-maxage=30, stale-while-revalidate=120 is
present on anonymous /. It remains inert at the edge until the Cloudflare
rule is applied — the one item in this report that cannot be fixed from the
repository. See 05 §4.
Not done, and why¶
Two items are deliberately left. Each is a real cost, not an oversight:
Item |
Why not |
|---|---|
Split |
The win is parse/style-recalc time on low-end devices, and verifying it needs three themes × two widths × ~30 full-screen apps looked at by eye. Doing it blind risks a visual regression across the whole product to save 48 KB brotli. |
The 3D games’ 4.8–6.0 s of long tasks |
The highest ceiling in the product and the only item that is genuine investigation rather than a known fix — three.js shader compilation, scene construction, geometry upload, profiled per title. |
Contents¶
File |
What is in it |
|---|---|
The harness, raw numbers, per-route tables, and the measurement traps |
|
JavaScript: the icon chunk, framer-motion, socket.io, zod, fragmentation |
|
The API layer: the safety triage, what was cached, and what must stay uncached |
|
Why the schema does not need a revamp, with the evidence |
|
Cold start, the |
|
What is still open, ranked, with the measurements that killed three former entries |