Skip to content

overhaul: assets & performance (media pipeline, Lighthouse CI budgets) - #25

Open
CS-5 wants to merge 3 commits into
overhaul/08-eventsfrom
overhaul/09-assets-performance
Open

overhaul: assets & performance (media pipeline, Lighthouse CI budgets)#25
CS-5 wants to merge 3 commits into
overhaul/08-eventsfrom
overhaul/09-assets-performance

Conversation

@CS-5

@CS-5 CS-5 commented Sep 1, 2026

Copy link
Copy Markdown
Member

Phase 09 of the stacked overhaul (plan/09-assets-performance.md), stacked on #24 per D23.

What this does

public/ + src/assets/: 72 MB → 11 MB.

  • public/image/ is deleted in full — 33 files, 5.4 MB. Nothing in src/, functions/, tools/, or the built output referenced any of them: the unDraw set retired with DESIGN.md §7, the pattern SVGs with the engineering grid (§2.3), meta.png with public/og/default.png, and the rest are Docusaurus-era leftovers. Everything a page renders had already moved to src/assets/ in Phase 04.
  • The hero video goes from 22.1 MB MP4 + 14.7 MB WebM to 1.19 MB + 0.49 MB — a 10.5 s cut at 720p, no audio track, +faststart. Encode parameters and reasoning in ADR 0006; largest remaining image source is 470 KB.

Images.

  • New HeroImage.astro. Eleven heroes were repeating the same six <Image> attributes; they now render one component. class="h-full w-full object-cover" went with them — Hero's own style already sizes whatever lands in its media slot.
  • Every photographic call site carries PHOTO_QUALITY, and every responsive one carries an explicit width equal to its widest widths entry. Without it Astro fills the src fallback from the source's intrinsic size, which was emitting a 436 KB variant no page displays. Largest emitted image is now 199 KB.
  • Width ladders gained a step near 768px — Lighthouse's mobile viewport asks for ~721 device px, and two-step ladders were answering with a 1120px or 1280px variant. Real phones sit in the same band.

Fonts. Two changes, and they are what actually met the LCP budget:

  • The two <link rel="preload"> font tags are gone (ADR 0008). 61 KB of High-priority font was queued from <head> ahead of the render-blocking stylesheet and well ahead of the hero <img>, on pages whose LCP element is always a photograph. Removing them: FCP 1054 → 766 ms, LCP 2028 → 1366 ms on /programs/frc/robots/, CLS 0.000 either way.
  • Inter's weight axis is trimmed to 400–700 (ADR 0011). @fontsource-variable/inter ships wght 100 900; DESIGN.md §3 sanctions 400/500/600 and nothing in the tree exceeds it — the two font-bold call sites are Orbitron. 47.1 KB → 35.2 KB on the critical path, 83.1 KB → 57.9 KB for the latin-ext face beside it. pnpm assets:fonts, output committed, and DESIGN.md §3 now notes that Inter cannot render heavier.

Hero video behaviour (D20). HeroVideo.astro renders the poster as a real <Image> — so it gets a responsive ladder, which poster= cannot — with the video over it at preload="none". Playback arms only after load, and only when the visitor has neither asked for reduced motion nor turned on Save-Data.

Lighthouse CI is a blocking check. .github/workflows/lighthouse.yml + lighthouserc.json, six URLs, three runs each.

Measured

Every assertion passes, on the median of three runs:

URL Perf A11y BP SEO LCP median LCP, the three runs CLS TBT Transfer
/ 99 100 100 100 1807 ms 1355 / 1807 / 1819 0.000 0 165 KB
/programs/frc/ 100 100 100 100 1659 ms 1658 / 1659 / 1663 0.000 0 623 KB
/programs/frc/robots/ 100 100 100 100 1430 ms 1355 / 1430 / 1881 0.000 0 256 KB
/sponsors/ 100 100 100 100 1282 ms 1210 / 1282 / 1953 0.000 0 206 KB
/openhouse/ 99 100 100 100 1807 ms 1657 / 1807 / 1808 0.000 0 123 KB
/contact/ 100 100 96 100 1659 ms 1658 / 1659 / 1663 0.000 0 109 KB

/contact/'s Best Practices is 96 because of the Turnstile widget's third-party script; the threshold is 0.95 and the widget is the form's spam defence.

How the LCP budget was met, and why the aggregation says median. Before trimming Inter, LCP was bimodal — clusters around 1.2 s and 2.1 s with nothing between — and /programs/frc/robots/ and /sponsors/ each had runs above the 2000 ms budget (1355 / 2108 / 2110 and 1210 / 2032 / 2036). Chasing it structurally got nowhere: an LCP-image <link rel="preload"> moved the median by 2 ms. Taking 12 KB off the critical path is what collapsed it. Every run on every budgeted URL is now under 2000 ms, worst 1953 ms, so the gate asserts on the median rather than the best run — which is what the budget was always supposed to mean.

Deviations from the brief, each with an ADR

  • §2's "AVIF+WebP formats" is WebP only — ADR 0005. AVIF needs sharp's default effort to beat WebP, at ~11 s per variant against ~0.3 s; across 172 variants that is half an hour on every build and every CI run. At affordable effort levels the two land within 3% of each other, and Astro's <Picture> exposes no per-format effort knob. Full measurement table in the ADR.
  • §4's "exactly two preloaded font files" is zero — ADR 0008, above.
  • @lhci/cli runs through pnpm dlx at a pinned version, not as a devDependency — ADR 0007. It brings Lighthouse and Puppeteer, for a tool nobody runs while writing a page.

Notes for the reviewer

  • tools/assets/optimize-sources.mjs is at its fixpoint — a dry run reports zero changes across the relocated inventory. That is the "refuses changes with no meaningful gain" floor working as designed; the re-encode §2 asks for happened in Phase 04.
  • The robots page's loading={index === 0 ? "eager" : "lazy"} was left alone. It reads like a below-the-fold eager fetch, but index 0 is a season with no photo yet, so the eager branch never renders — confirmed against the built HTML, where every robot image is lazy.
  • Two manual asset steps, neither in pnpm build: the video encode (ffmpeg, ADR 0006) and the font instancing (pnpm assets:fonts, needs Python with fonttools and brotli, ADR 0011). Both change when the source material or DESIGN.md §3 changes, which is not per-build. Masters live in git history.
  • Orbitron and Source Code Pro were measured and left alone — axis-trimming saves 0.7 KB and 3.2 KB, not worth narrowing what the design may reach for.
  • Budget table in docs/tooling.md; pnpm check && pnpm build green.

Verification

Lighthouse driven against the production preview (astro preview, which gzips — verified, so the measurement is comparable to what Cloudflare serves) in the pre-installed Chromium. The trimmed Inter was checked in a browser to confirm all four weights interpolate rather than synthesize (measured glyph-run widths at 400/500/600/700: 521 / 531 / 538 / 542 px).

Deletes public/image entirely (33 files, 5.4 MB — nothing in src/ or the
build referenced any of them), re-encodes the hero video from 36.9 MB across
two formats to 1.7 MB, and makes Lighthouse CI a blocking PR check.

Images: HeroImage.astro replaces six identical attributes at eleven hero call
sites, every photographic <Image> carries PHOTO_QUALITY, and every responsive
one carries an explicit width so Astro stops emitting a full-resolution
fallback nobody displays. Largest emitted variant 436 KB -> 199 KB.

Hero video: 720p H.264 + VP9 of a 10.5 s cut, behind HeroVideo.astro, which
arms an IntersectionObserver only after load and only when the visitor has
neither asked for reduced motion nor turned on Save-Data.

Three ADRs record where the measurements contradicted plan/09: WebP-only
variants (0005), the video encode (0006), the Lighthouse gate's shape (0007),
and the font preloads that were costing ~660 ms of LCP (0008).

Medians of three runs: Perf 99-100, A11y 100, SEO 100, BP 96-100, CLS 0.000,
TBT 0, LCP 1210-1958 ms across the six budgeted URLs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CxssDiw8XBERsuHMQZTtCr
Three runs per URL produce a bimodal LCP under Lantern — either ~1.2 s or
~2.0 s, nothing between, tracking a ~750 ms step in simulated FCP that appears
run to run on an idle machine. Five runs of /programs/frc/robots/: 1212, 1218,
2039, 2109, 2110 ms.

lighthouserc.json now sets aggregationMethod: "optimistic" rather than
inheriting it as a default, so the gate's meaning is stated: best of three has
to clear 2000 ms. Two of the six budgeted pages have runs above it, which the
phase's As-built table now reports as run triples instead of a median that
would read as a contradiction of a green gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CxssDiw8XBERsuHMQZTtCr
@fontsource-variable/inter ships wght 100 900. DESIGN.md §3 sanctions
400/500/600 for Inter and nothing in the tree exceeds it — the two font-bold
call sites are Orbitron — so more than half the axis was delta data for weights
that never render. Instancing to 400 700 takes the face on the critical path
from 47.1 KB to 35.2 KB, and the latin-ext face it sits beside from 83.1 KB to
57.9 KB.

That 12 KB is what collapsed the bimodal LCP. Before: clusters around 1.2 s and
2.1 s with nothing between, and two of the six budgeted URLs had runs above the
2000 ms budget. After: every run on every URL is under it, worst 1953 ms. So
lighthouserc.json asserts on the median of three runs rather than the best,
which is what the budget was always supposed to mean.

Orbitron and Source Code Pro were measured and left alone: 0.7 KB and 3.2 KB,
not worth narrowing what the design may reach for. DESIGN.md §3 gains a note
that Inter can no longer render above 700, since that is where someone would
look before reaching for a heavier weight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CxssDiw8XBERsuHMQZTtCr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants