Teardown

Lighthouse gave our broken checkout 100, ten times. Its report listed both charges

We ran Lighthouse 30 times on two live pages and a planted checkout that charges twice. The score measures loading well. It moved 25 points with no code change.

Published Updated 5 min read

A Lighthouse 100 is an honest claim about how a page loads and no claim at all about whether it works. That sounds obvious, until a perfect score ends up as a merge gate or a launch checklist item. We ran it 30 times this morning. A planted checkout that charges the card twice on every load scored 100 for performance ten runs in a row. Our own homepage moved 25 performance points between two runs with no code change. Use the score as a load budget, read it as a median, and never let it stand in for a test.

Why now

"Removing Next.js got my blog to 100 on every Lighthouse category" is on the Hacker News front page today, and the thread under it has turned into a roll call of perfect scores. benmccann: "I got straight 100s using SvelteKit" (comment). SwellJoe, on a static blog: "I just checked and it's 100 on pagespeed. The magic of a static HTML blog is you don't have to think about performance" (comment).

Both are fair. A static page that scores 100 really is fast. The question for anyone shipping product, not a blog, is what else the 100 is taken to mean.

What we ran

Lighthouse 13.5.0 from npm, headless Chrome on macOS, default settings: mobile form factor, simulated throttling. Ten runs each, back to back, on three targets:

  1. Our homepage, a Next.js page, live.
  2. One of our blog posts, live.
  3. A local checkout page we wrote for this. It is plain HTML, one serif font, a labelled card field and a Pay button. Its Pay handler swallows the submit and does nothing. Its script sends POST /api/charge twice on page load. Our local server answers 200 and counts each charge.
TargetPerformance, 10 runsAccessibilityBest practicesSEO
Homepage65, then 89 to 90 (median 89)96 every run77 every run100 every run
Blog post81 to 97 (median 91)95 every run77 every run100 every run
Planted checkout100 every run100 every run96 every run100 every run

The server logged 20 charges across the 10 checkout runs: two per page load, each answered 200.

What it got right

The three non-performance categories are deterministic. Accessibility, best practices and SEO gave the same number on all ten runs of every page. They are rule checks on the rendered page, and they behave like rule checks.

It found a real cost on our own site. Best practices sat at 77 on both live pages. The reason was not our code: our session-analytics script sets third-party cookies, and Lighthouse lists every one, by name and domain. That is a trade we made on purpose, and the score is right to charge us for it.

The performance metrics are useful as numbers. Largest contentful paint on the homepage settled at about 3.5 seconds after the first run. That is a clear, actionable figure.

Where the score moves

The homepage's first run scored 65, with an LCP of 8.6 seconds. The next nine scored 89 or 90, with LCP between 3.5 and 3.7 seconds. The blog post's first run was also its worst, at 81, and its best was 97. The first run of each live page being the slowest suggests a cold cache somewhere between us and the server. We did not prove that.

Lighthouse's own documentation expects this. Its variability guide says performance scores "will change due to inherent variability in web and network technologies, even if there hasn't been a code change", and tells you to run it multiple times "before drawing conclusions" (docs). A gate on one run of the performance score is a flaky test by design: on our homepage, a threshold of 80 would have failed one run in ten with nothing changed.

What it cannot see

The checkout is the sharp result. It scored 100 for performance, accessibility and SEO on every run, and lost four best-practice points to a missing favicon. The page cannot take a payment, and it charges the card twice before anyone touches it.

The evidence was in the report. The network-requests table lists both POST /api/charge calls, each with status 200. No audit reads that table for meaning, because no audit knows that one page load should make zero charges. That is not a Lighthouse bug. It loads the page and measures it; it never clicks Pay, and it has no idea what Pay is for.

We saw the same shape this morning in impeccable's detect CLI: a static check gives a clean bill to a page that does the wrong thing. Lighthouse adds a twist. It recorded the wrong thing, and then scored it perfect.

Verdict

Keep Lighthouse. Gate on accessibility and best practices, which are stable and catch real problems. Treat performance as a budget: run it five or more times and compare medians, or you will be retrying a flaky gate. And keep a separate check for what the page is for, one that clicks Pay and counts the charges. A perfect score told us the broken checkout was fast, accessible and well indexed. All three were true.

Sources

  1. 01korindiehacker: Removing Next.js got my blog to 100 on every Lighthouse categorykorindiehacker.bloffy.blog
  2. 02Hacker News: discussion threadnews.ycombinator.com
  3. 03Hacker News: SwellJoe, on a static blognews.ycombinator.com
  4. 04Hacker News: benmccann, straight 100snews.ycombinator.com
  5. 05GitHub: GoogleChrome/lighthouse, Score Variabilitygithub.com
  6. 06GitHub: Lighthouse v13.5.0 releasegithub.com
Reticleverdict

// The agent clicked "Pay". Did the right things actually happen?

{verified: "no"failureReason: "POST /api/order returned 500, expected 200"source: "src/checkout/PayButton.tsx:42"}

Example from the README

Your agent says it works. Reticle checks the network, state and console inside your running app, and names the line to fix.

85 of 88 seeded bugs caught in the public benchmark