`formFactor: 'desktop'` changes which thresholds a metric is scored against and NOTHING else. Throttling and the user agent keep their defaults, which are mobile — so every desktop run was a desktop-sized window on a 4x-slowed CPU over slow 4G, announced as a moto-g. It read 86 where the same page reads 100. That gap was reported repeatedly as network latency to the host. It was not latency; it was this. Measured after the fix: desktop 100, LCP 0.4s against 2.2s. Nine more from the same review: runtimeError was never checked, so a page that failed to load reported "nothing failing" — the most reassuring possible output for a site that is down --json exited 0 before the gate, so the documented non-zero exit was true for a person reading the table and false for the only caller that could act on it scoreDisplayMode 'error' was filtered out while still nulling its category, so a category could read 0 with an empty findings list a missing metric serialised as 0, which a script reads as a perfect measurement `source` is a string on some audits and an object on others; String() on the object printed [object Object] the watchdog's surrender was permanent — the health branch sat below the check, so once it stopped trying it also stopped noticing, and a server fixed by hand left it asleep until the platform restarted a failed restart leaves no process at all, because startServer deletes before it starts. status null then reads as "somebody stopped it", which the watchdog leaves alone — one bad restart would have turned it off and taken the server with it, silently the skill claimed the panel uses this engine. Nothing but the CLI does and gave `bun run build:site` with no cwd, which only works from the website checkout — api/static-site.ts documents that and the skill did not Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
134 lines
6.8 KiB
Markdown
134 lines
6.8 KiB
Markdown
---
|
||
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. The engine is `api/lighthouse.ts`, which is where the panel will read
|
||
from when it grows a screen for this — today the CLI is the only caller.
|
||
|
||
## 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" <url>
|
||
```
|
||
|
||
| 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: build first, then audit the public URL. The build has to run in the website checkout and with the
|
||
site's own environment — see `api/static-site.ts`, which documents both:
|
||
|
||
```bash
|
||
cd ~/officerdev/data/*/FofosWebsite/website && bun run build:site
|
||
```
|
||
|
||
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 audit is desktop in four ways, and `formFactor` is only one of them.** Setting
|
||
`formFactor: 'desktop'` changes which thresholds a metric is scored against and NOTHING else — the
|
||
throttling and the user agent keep their defaults, which are mobile. A run configured that way is a
|
||
desktop-sized window crawling on an emulated phone, and it reads about 86 where the same page reads
|
||
100. `api/lighthouse.ts` sets `screenEmulation`, `throttling` and `emulatedUserAgent` together for
|
||
this reason; if you write your own runner, set all four or the number is about nothing.
|
||
|
||
**A number from another machine includes that machine's network.** 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 — though check the paragraph above first, because that explains far more of a gap than
|
||
latency usually does.
|
||
|
||
**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 `<picture>` will actually pick.** Given `href`
|
||
alone it fetches one file and the `<img>` 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.
|