--- name: fofos-lighthouse title: Measuring the fofos site description: Use when working on the fofos website's performance, accessibility, SEO or Core Web Vitals — running an audit, reading what it found, and the traps that make a number mean something other than it appears to. version: 1 --- `bin/lighthouse.ts` runs Lighthouse against a URL in a real Chrome and prints what failed, with the resources each finding names. It is the same engine the panel uses (`api/lighthouse.ts`), so a number you get here is the number the panel would show. ## Running it ```bash # The platform checkout is named differently per machine — officer-dev here, officerdev on fofos — # so the glob finds it either way rather than the doc being right on one box and wrong on the other. bun "$(ls -d ~/officer*/platform/plugins/fofos-admin | head -1)/bin/lighthouse.ts" ``` | flag | effect | | --- | --- | | *(none)* | mobile and desktop, in that order — about 20 seconds | | `--mobile` / `--desktop` | one form factor | | `--json` | the whole result on stdout; progress goes to stderr, so the pipe stays clean | | `--quiet` | no progress narration | Exit code is 0 only when every category on every form factor is 100. Non-zero otherwise, so it can gate something without being parsed. ## Measure the BUILT site The dev server at `localhost:3011` renders from the database on every request. The public site is static files under `dist/`, served by nginx behind a proxy, with different caching, different compression and a trailing-slash redirect. **They are not the same page** and their numbers do not agree. So: `bun run build:site` first, then audit `https://web.fofosfluffybastards.com/...`. A score from the dev server is not wrong, it is about something nobody visits. ## Reading the output ``` ═══ MOBILE https://web.fofosfluffybastards.com/gallery/aldi/ ✓ performance 99 ✓ accessibility 100 ✓ best-practices 100 ✓ seo 100 First Contentful Paint 1.0 s Largest Contentful Paint 2.2 s Total Blocking Time 0 ms Cumulative Layout Shift 0 Speed Index 1.0 s [ 0] Improve image delivery — Est savings of 47 KiB …/ALDI_LIDL_0000_1.png/960.avif 47 KiB wasted, 57 KiB served, li.w-[70%] > … > img.h-full ``` The bracketed number is that audit's own score out of 100. The indented line is the resource — a URL, what it cost, and the CSS path to the element. **That last part is what makes a finding fixable**: it tells you which block emitted it, which tells you which component to look at. Only scored audits appear. `informative`, `manual` and `notApplicable` are filtered out — they never had a score, and listing them turns a report into noise nobody reads. ## The five metrics, and which one your change moved Performance is computed from these and nothing else. Insights are diagnostics; they do not score. - **Total Blocking Time** — JavaScript. Mobile throttles the CPU 4×, so a library parsed at load shows up here and nowhere else. Moving 56 KB of GLightbox to load-on-intent took this from 130 ms to 0. - **Largest Contentful Paint** — usually the biggest image above the fold. Wants three things together: preloaded, `fetchpriority="high"`, and NOT `loading="lazy"`. Missing any one is a separate failing audit. - **First Contentful Paint** — the fonts, mostly. `font-display: block` holds text back until a face arrives, so the preloads in `Layout.tsx` are what keep this honest. They went missing once and cost 200 ms on every page but Home. - **Cumulative Layout Shift** — anything sized after it paints. Should stay 0. - **Speed Index** — how fast the page fills in. Follows from the other four. ## Traps **A desktop number from another machine includes that machine's latency.** Auditing from officer-dev against fofos crosses a network the owner's browser does not. Compare like with like: two runs from the same place, before and after a change. An absolute score from somewhere else is a different question. **Run-to-run variance is real** — a point or two either way on the same build. Chase a finding, not a number. If a score moves and no finding changed, nothing happened. **`sizes` is not a decoration.** It tells the browser which rung to fetch before layout exists, so a `sizes` that overstates a column downloads a file two rungs too big and looks perfect. `columnSizes` in `components/PageSection.tsx` computes it from the same spacing the band lays itself out with, precisely so a padding change in the Layout panel cannot silently break it. **A preload must name the format and the rung the `` will actually pick.** Given `href` alone it fetches one file and the `` then chooses another — two downloads of the largest thing on the page, which is worse than no preload. `imagesrcset`, `imagesizes` and `type` all travel together or none of them do. ## What this has already caught on this site Worth knowing, because each was invisible until something measured: - a `sizes` string claiming a 624px column that renders 546 — the old constant assumed the band had no padding, so every cover fetched a rung too big - the font preloads gone entirely, while the comment explaining them survived in `Layout.tsx` and `build-fonts.ts` still pointed at a file that had been moved - an LCP image left `loading="lazy"` with no preload, because a rename from `collection-cover` to `subject-cover` missed the line that hunts for it — a string comparison the type checker cannot see The pattern: all three were renames or settings changes that left something behind, and none produced an error. That is what this is for. ## The browser Looked for in three places before anything is downloaded — `CHROME_PATH`, chrome-launcher's own search, then Chrome-family binaries by name (Brave counts; it is Chromium underneath). Only if all three miss does `@puppeteer/browsers` fetch one into `~/.cache/officer/browsers`, about 150 MB, once. If an audit fails with something about a browser, that chain is where to look — `chromePath()` in `api/lighthouse.ts` narrates which one it chose on every run.