Skip to content
Back to skills

Audit Page Load

BSecurity

Run a Lighthouse page-load audit of the Splotch web app emulated on a slow device + slow internet (phone portrait + tablet landscape), capturing first-visit and repeat-visit runs, and turn the results into audit opportunities. Use when asked to audit page-load/Lighthouse performance, measure load on a throttled device/network, check Core Web Vitals (FCP/LCP/TBT/CLS), or compare first vs repeat visit. For drawing/canvas interaction performance use the `profiling` skill instead.

  • 7 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
researchrustgobashnoderailsgitperformance

Works with

  • claude code
  • cli

Security analysis

B75/100
  • criticalAccesses sensitive system or user directories

Pro scans all 3 files and shows the line behind each finding

Scanned October 6, 2026

npx -y skills add KyleMit/Splotch --skill audit-page-load --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Audit Page Load?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Audit Page Load
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/kylemit-audit-page-load/badge)](https://www.skillsdirectory.com/skills/kylemit-audit-page-load)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: audit-page-load
description: Run a Lighthouse page-load audit of the Splotch web app emulated on a slow device + slow internet (phone portrait + tablet landscape), capturing first-visit and repeat-visit runs, and turn the results into audit opportunities. Use when asked to audit page-load/Lighthouse performance, measure load on a throttled device/network, check Core Web Vitals (FCP/LCP/TBT/CLS), or compare first vs repeat visit. For drawing/canvas interaction performance use the `profiling` skill instead.
---

# Splotch — Lighthouse page-load audit

Measures **page load** (how fast the app becomes usable) on a throttled device, as opposed to the
`profiling` skill, which measures **drawing interaction** once loaded. The LCP element is the full
drawing surface (currently `.paper-sheet`; earlier builds used `<canvas#drawingCanvas>`) — "loaded"
means that surface has painted. Confirm the element from each report instead of assuming it stayed
the same across UI changes.

The driver [`run-audit.mjs`](run-audit.mjs) runs Lighthouse via `npx lighthouse@12` (no install
needed) for two form factors × two visit types under a fixed slow-device/slow-internet profile,
writes JSON + HTML reports, and prints a summary table.

## Quick start

```bash
# Full matrix (phone + tablet, first + repeat) against production:
node .claude/skills/audit-page-load/run-audit.mjs

# One device / one visit:
node .claude/skills/audit-page-load/run-audit.mjs --device phone --visits first

# Audit a branch preview before merge (see "Which target" below):
node .claude/skills/audit-page-load/run-audit.mjs \
  --url https://claude-my-branch--splotchy.netlify.app/
```

| Flag            | Values                        | Default                            |
| --------------- | ----------------------------- | ---------------------------------- |
| `--url`         | any URL                       | `https://splotch.art/`             |
| `--device`      | `phone` \| `tablet` \| `both` | `both`                             |
| `--visits`      | `first` \| `repeat` \| `both` | `both`                             |
| `--out`         | directory                     | `lighthouse-reports/` (gitignored) |
| `--storage`     | `key=value;key=value`         | none                               |
| `--save-assets` | flag                          | off                                |

**Startup-setting variants (`--storage`).** A returning child's device boots from persisted settings
— dark theme, bare toolbar, open drawer, a bigger button scale — and each changes the first paint
(bare mode even moves the LCP element from `.paper-sheet` to `.rail-glass`). The flag seeds
localStorage for the audited origin into the Chrome profile before that device's runs
(`seed-storage.mjs`, through a page whose every request is answered locally, so the HTTP cache stays
cold). Lighthouse 12 resets only file systems, shader cache, service workers, Cache Storage and the
HTTP cache between runs, so the seed survives into a first visit. Keys are the `STORAGE_KEYS` values
in `web/src/lib/storageKeys.ts`; confirm the seed took by decoding the report's `final-screenshot`
(a dark run averages well under 100 per channel). Example:

```bash
node .claude/skills/audit-page-load/run-audit.mjs --device phone --visits first \
  --storage "splotch-theme=dark;splotch-toolbar-style=bare;splotch-drawer-open=true"
```

**Reading a long task (`--save-assets`).** The flag keeps `<name>-0.trace.json` beside the report.
Lighthouse's `long-tasks` and `bootup-time` attribute a task to the *first script URL on the stack*,
which on this app is whichever tiny chunk happens to run first (a 670-byte safe-area helper got the
whole 206 ms boot task). The trace's `RunTask` children tell the truth: `v8.evaluateModule` is
module evaluation (the engine boot), `RunMicrotasks` under
`BlinkScheduler_PerformMicrotaskCheckpoint` is SvelteKit hydration, and Lighthouse's trace carries
no CPU samples. For function-level attribution run `npm run perf:web:mount` (its trace has the V8
sampler) on a build made with `node tools/run-web-tool.mjs vite build --minify false` (then
`--no-build`), and for the caller of a forced style recalc trace with
`disabled-by-default-devtools.timeline.stack` so `UpdateLayoutTree` events carry a stack; the
sampler's clock trails the trace clock by up to ~20 ms, which is enough to blame the wrong native
call.

**What's fixed** (the "slow device + slow internet" definition): Lighthouse's default mobile
emulation — **simulated Slow 4G + 4× CPU throttle** — for every run. Form factors: **phone
portrait** 412×915 (dsf 2.625), **tablet landscape** 1133×744 (dsf 2). Repeat visit is a genuine
warm-cache load: the driver primes a persistent Chrome profile with a first pass, then re-runs with
`--disable-storage-reset` so the disk cache carries over.

## Which target to audit — this matters

There are three things you can point Lighthouse at; they answer different questions. **For any score
you report or track, use production (or a branch preview) — nothing else is accurate.**

| Target                                             | How                            | Use it for                                                                                                                                                                                                                                                                                                                                    |
| -------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Production** `splotch.art`                       | default                        | The real number. Real minified bundles + Netlify CDN + HTTP/2 + real cache headers.                                                                                                                                                                                                                                                           |
| **Branch preview** `<slug>--splotchy.netlify.app`  | `--url`                        | Production-accurate numbers for **unmerged** work (same Netlify serving). The way to measure a perf change before merge. In restricted preview mode only `feature/*` branches deploy — see [Claude Code Cloud § Two preview modes](../../../docs/CLOUD/Claude-Code.md#two-preview-modes--check-which-one-is-active) for how to get a preview. |
| **Local prod preview** `npm run build` + `preview` | `--url http://localhost:4173/` | Measuring the *bundle* in isolation (bytes, DOM count). **Ignore its LCP/network/score** — its HTTP/1.1 no-CDN serving is unrepresentative.                                                                                                                                                                                                   |

**Do not audit the dev server (`npm run dev`) for scores.** It serves hundreds of unbundled ES
modules with the HMR client and dev-mode Svelte checks — that distorts the request graph, TBT and
LCP structurally, not by a constant, so the numbers map to nothing shippable. (Real gotcha: an early
local-preview run here reported LCP 5.4 s / Perf 73; the same build on production was LCP 1.9 s /
Perf 91 — the preview's serving layer was the whole gap.)

For the *diagnostic* questions production is bad at, don't reach for the dev server — use the
better-suited tool:

* **Which JS/CSS is unused, and where in source** → audit the **preview build with source maps**
  (real bytes + readable attribution).
* **Why is the main thread busy / drawing jank** → the `profiling` skill (`npm run perf:web`), which
  profiles the production bundle with named marks.

## Running from a Claude-Code-on-web sandbox (the proxy/TLS workaround)

The driver handles this automatically — this section is *why*, so it can be fixed if it ever breaks.
The cloud sandbox's outbound egress is a **TLS-terminating MITM proxy**. `curl` works (it trusts the
CA bundle), but headless Chrome fails to load an external https origin unless three things are true;
the driver adds these `--chrome-flags` only when it detects the sandbox (`HTTPS_PROXY` set, Linux):

1. **Route through the proxy** — `--proxy-server=$HTTPS_PROXY`. Without it Chrome tries a direct
   connection that the sandbox has no egress for.
2. **Trust the MITM leaf** — `--ignore-certificate-errors-spki-list=<hashes>`. The proxy presents a
   per-host cert signed by an Anthropic CA in `/root/.ccr/ca-bundle.crt`; the driver computes the
   base64-SHA-256 SPKI of those signing CAs and allowlists them (matching any cert in the chain is
   enough). Plain `--ignore-certificate-errors` does **not** work: the site sends HSTS, which makes
   cert errors non-bypassable.
3. **Force TLS 1.2** — `--ssl-version-max=tls1.2`. **This is the key fix.** The gateway RESETS
   Chrome's TLS 1.3 ClientHello (`ERR_CONNECTION_RESET`); TLS 1.2 negotiates cleanly.

Diagnose proxy issues with `curl -sS "$HTTPS_PROXY/__agentproxy/status"`. This only affects
*reaching* the origin — Lighthouse's `simulate` throttling models the network from the request
graph + byte sizes, so the proxy path and TLS version do **not** skew FCP/LCP/transfer numbers.
Off-sandbox (a normal laptop) the driver adds none of this and runs a plain Lighthouse.

## After every run — do this every time (not optional)

These steps are part of *running* the skill, so a caller can just invoke it and get them for free.
Do them in order.

### 1. Read the results

`--out` holds `<device>-<visit>.report.{json,html}`. Open the HTML for the full report; the console
summary covers the headline scores. For attribution beyond the headline (which node blew up DOM
size, LCP phase breakdown, main-thread cost by category) read the JSON — e.g.
`node -e 'const a=require("./lighthouse-reports/phone-portrait-first.report.json").audits; …'` lets
you pull `dom-size`, `largest-contentful-paint-element`, `mainthread-work-breakdown`, and
`bootup-time` `details.items` without opening the 464 KB HTML. Always inspect `long-tasks` too. If
every item is attributed to `_lighthouse-eval.js`, the reported TBT and derived performance score
include Lighthouse's own injected work; the driver marks those TBT values with `*`. Do not turn that
value into an app finding — use `npm run perf:web:mount` for an independent startup trace first.

### 2. Merge findings into `docs/AUDIT.md` — combine, don't overwrite

This is an audit skill; it follows the shared conventions in
[`.claude/audit-conventions.md`](../../../.claude/audit-conventions.md). Turn opportunities into
`docs/AUDIT.md` findings under a `## Source: Lighthouse page-load audit` section, using the
canonical finding format documented there. **Merge into that section — do not clobber it (§1):**

* **An existing item still stands** → keep it; *enrich* it with any sharper attribution this run
  gave you (e.g. a specific file/node the report now points at), and refresh its numbers.
* **The score table** → update it to reflect this run. Prefer showing a *range* across runs over
  replacing the single number, and note the audit date — that preserves the variance picture instead
  of erasing the prior data point.
* **A genuinely new opportunity** → add it as a new `###` finding (canonical format).
* **An item that's since been fixed** → remove its whole `###` block (confirm against the report
  first).

### 3. Log the run

Add an entry to `docs/AUDIT-LOG.md` (§2 of the shared conventions) — today's date,
`audit-page-load`, and a one-line summary (headline scores + the standout lever).

### 4. Self-heal this skill

If anything surprising surfaced that a future caller would want to know — a durable **method**
gotcha (a false-positive audit, a proxy quirk, an interpretation trap) — fold it into this
`SKILL.md` as part of the same task (§3 of the shared conventions). **Do not** record the specific
findings/opportunities here: those go to `docs/AUDIT.md`, which `vet-audits` drains into
`type:audit` GitHub issues, so a copy in the skill would only go stale. The skill carries *how to
audit and how to read the numbers*; the findings live in `docs/AUDIT.md` (then the issues) as
*what's currently wrong*.

## Interpretation caveats

**False positive — Lighthouse's own work can dominate TBT:** Lighthouse labels its
`Runtime.evaluate` scripts `_lighthouse-eval.js`. With simulated CPU throttling, that injected task
can be scaled and counted in TBT even though Lighthouse excludes the same URL from `bootup-time`.
This reproduced in both Lighthouse 12.8.2 and 13.4.0. When every `long-tasks` item has that URL,
treat TBT and the derived Perf score as contaminated, inspect the observed `main-thread-tasks`, and
confirm any startup concern with `npm run perf:web:mount` before filing it. If an app URL also
appears, investigate that app-attributed work normally.

**False positive — don't file an audit item for it:** the `lcp-discovery-insight` audit currently
flags *"`fetchpriority=high` should be applied: false"*. The LCP is `.paper-sheet`, whose tiny CSS
background texture is already discoverable in the initial document and requested at high priority;
the `<div>` cannot itself take `fetchpriority`. Earlier builds used the painted canvas and had no
LCP resource at all. Confirm the current LCP node plus `lcp-breakdown-insight` and request priority;
only file this if a future LCP resource has meaningful discovery delay.

**Sandbox Chrome:** the cloud sandbox's Playwright Chromium lives at
`/opt/pw-browsers/chromium-<rev>/chrome-linux/chrome` (older downloads used `chrome-linux64`); the
driver checks both and the root `chromium` symlink, and exits 1 when no run produced a report — an
earlier version exited 0 with four failed cells, which a caller reading only the exit code took as
success.

**Variance:** `simulate` mode is not deterministic — Perf can swing ±15 points and TBT can double
between identical runs (observed: phone-first Perf 84↔91, TBT 360↔560 ms across two production
audits). After ruling out the self-attribution artifact above, judge trends and medians across a few
runs, not a single number; don't treat a one-off swing as a regression.

Reports are large; the output dir is gitignored. **Do not commit them** — attach the HTML (zipped;
GitHub rejects raw `.html`) to the PR instead.

Files in this skill

  • SKILL.md14.4 KB
  • run-audit.mjs13.3 KB
  • seed-storage.mjs2.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…