DEV Community

Cover image for Lighthouse says 86. Run it again: 91. Building a free local console for scores you can defend
Simon
Simon

Posted on Originally published at lightauditscore.com

Lighthouse says 86. Run it again: 91. Building a free local console for scores you can defend

You know this loop.

A page feels slow. You open the Lighthouse panel in DevTools, hit Analyze, and get 86. You change nothing, run it again, and get 91. You run it a third time out of spite: 78.

Now which number goes in the PR description?

This isn't a bug. Total Blocking Time is CPU-sensitive and worth roughly 30% of the Performance score, so anything else your laptop is doing — a Slack notification, a Docker build, Spotlight reindexing — moves the number. Lighthouse Performance realistically swings about ±5 points on identical runs of an identical page.

One run is an anecdote. And the tool that would give you a stable, real-world answer — PageSpeed Insights — needs a public URL, so it can't audit the thing you're actually working on.

I got tired of this and built LightAudit Score: a local console that runs Lighthouse on your own machine, repeats it enough times to mean something, and keeps the results.

It's free. Not "free tier" — free, MIT, no account, no usage cap.


The three gaps, concretely

1. Reach: PSI needs a public URL, your work isn't public

PageSpeed Insights is excellent and I use it constantly. It also cannot audit:

  • localhost:3000, which is where the change you just made lives
  • a staging box behind a VPN
  • the internal app that nobody can link to
  • a preview deploy that dies in an hour

The usual workaround is a tunnel, or "we'll check it after deploy," which means checking it after it's a problem.

LightAudit runs the same Lighthouse v13 engine against your own Chrome. If your browser can open it, LightAudit can audit it — localhost, staging, VPN, intranet, all through exactly the same pipeline.

2. Accuracy: make the number boring

This is the part I care about most, because a score you can't reproduce is a score you can't act on.

  • Median of N. Every URL is audited N times (default 3), and Lighthouse's own computeMedianRun picks the representative run. Not the average — the actual median run, with its real trace.
  • Isolated Chrome per run. Every run launches a fresh headless Chrome with a cold profile. No warm cache flattering the second run.
  • One job per process. Every audit job runs in its own forked Node process, so two concurrent audits can't poison each other's timings.
  • Calibration. Lighthouse's default 4× CPU throttle is meant to simulate a mid-tier phone — from a reference machine. On a fast Mac, 4× under-throttles, and every score you produce reads optimistically. LightAudit reads your host's benchmarkIndex and picks the multiplier that actually targets mid-tier mobile from your machine.
  • Drift warnings. Every result carries the host power, throttling method and multiplier it ran under, and flags when the machine moved the score rather than the page: host-power drift, CPU contention, concurrency contention.

There's also a one-click "Match DevTools preset" — mobile, simulated throttling, single run, concurrency 1, Lighthouse's own 4× — for when you specifically want a number comparable to the DevTools panel on the same machine.

3. Memory: hosted tools forget the moment you close the tab

Every run is written to a local SQLite database, alongside the full Lighthouse JSON and HTML reports on disk. So you get history that survives restarts, per-URL trend sparklines, a two-run diff (Perf 71 → 89 ▲18, LCP 2.9s → 1.6s), batch pass/fail against your own thresholds, and CSV/JSON export.

It lives on your disk. Nothing is uploaded anywhere.


Lab and field in the same table

Lab scores tell you what the page does in a controlled run. Field data tells you what real Chrome users actually experienced. Normally those come from two tools with two separate histories.

LightAudit takes the same paste-or-crawl input and can run it through Google's hosted Lighthouse instead of your local Chrome, pulling real-user Core Web Vitals from CrUX — LCP, INP and CLS at the 75th percentile, per URL and per origin, with distribution bars sitting right next to the lab scores. PSI runs get a distinct badge in History so you never confuse a hosted number with a local one.

That path needs your own free Google API key (keyless PSI calls are capped at zero now). The app walks you through creating one; the free tier is 25,000 requests a day. The key is read server-side from your .env and never reaches the browser.


"Why is this 71?"

A score tells you something is wrong. It doesn't tell you what to do on Monday.

Click a category score and the AI reads the actual audit data — not a summary of it — and comes back with a diagnosis and prioritised fixes:

Diagnosis. LCP is 2.9 s because the hero image is a 1.4 MB PNG served at native resolution and discovered late by the preload scanner. TBT (380 ms) comes almost entirely from a single 640 kB analytics bundle that executes on the main thread before hydration.

Serve the hero as responsive AVIF and preload it · High — cuts the LCP resource by ~85% and lets the browser discover it before CSS resolves.

You bring the model. Three provider paths:

  • Claude — if you're already signed in with Claude Code on that machine, there's nothing to configure.
  • Ollama — a local model, fully offline and free. 14B+ with a large context works best.
  • Any OpenAI-compatible endpoint — OpenAI, OpenRouter, LM Studio, vLLM. Base URL, model id, key.

Every analysis is badged with the provider and model that produced it, and analyses are stored with the run, so reopening a report costs nothing.


The part where citations get real: plugging in CrawlForge MCP

Here's the honest limitation. An AI looking only at Lighthouse JSON can diagnose your page, but it can't tell you what web.dev currently recommends, or check whether the advice it's giving you is still accurate in 2026. When no research server is connected, LightAudit says so — fixes carry a plain No web research badge. It never invents a source.

Connect a research server and that changes: the AI can actually open pages and cite what it read.

LightAudit deliberately doesn't bundle, install, or credential a research server. CrawlForge MCP is the one named option, off by default, behind a switch in Settings — and the way it's wired is worth explaining, because it's where the interesting security problem lives.

The obvious design is to let you paste a key into LightAudit and forward it to the server. Don't do this. The Claude Agent SDK serialises the entire mcpServers config — env values included — into the claude CLI's argv as --mcp-config <json>. Anything you forward that way is visible to ps for the whole duration of an analysis, to every process on the machine.

So LightAudit forwards nothing. CrawlForge authenticates entirely from its own config, written by its own setup wizard:

npx crawlforge-setup     # validates the key, stores it in ~/.crawlforge/config.json
Enter fullscreen mode Exit fullscreen mode

LightAudit then existence-checks that file — it never opens it — and flips the switch in Settings from unavailable to available. The status object it builds is { enabled, setupOnDisk, available, active }: four booleans, not a single credential. The key never enters LightAudit's process, its database, its .env, or its argv.

The same argv caveat applies to any env block in your own .mcp.json, which is worth knowing whatever tooling you're wiring up. If you take one thing from this post and it isn't about Lighthouse, take that one.

Two more details that were deliberate rather than accidental:

The version is pinned. Launch is npx -y --prefer-offline crawlforge-mcp-server@5.6.0. An unpinned npx -y executes whatever was published last — install scripts included — with your privileges. Pinning means a CrawlForge release requires a reviewed LightAudit bump rather than arriving silently.

The agent only gets search-and-read tools. CrawlForge exposes 29; the analysis sees ten of them — search_web, scrape, fetch_url, extract_content, extract_text, extract_links, extract_metadata, summarize_content, analyze_content, reddit_search. Site crawlers, batch jobs, browser automation, monitoring and deep_research are denied outright. Every CrawlForge call spends credits from your account, and a citation search needs to find a page and read it — not crawl a domain. An agent that can call crawl_deep on a whim is an agent that can spend your money by accident.

Now when you ask "why 71?", the fixes come back with 4 sources and links to pages the model genuinely opened — web.dev/optimize-lcp, the Chrome Lighthouse docs — rather than URLs it pattern-matched from memory.

CrawlForge suits this slot because it's built for agents rather than humans: MCP-native tools that return clean markdown and structured JSON instead of a wall of nav chrome and cookie banners. New accounts get 1,000 free credits, no credit card, and by their published rates fetching or extracting a page runs 1–2 credits with a web search at 5 — so a thousand credits covers a great many analyses before you think about a plan.

If you'd rather run something else, the generic path is still there: declare any research server in a standard .mcp.json, name it research (or point LH_RESEARCH_MCP_SERVER at another name), and LightAudit will drive it. CrawlForge simply takes precedence when it's switched on.

A second, sneakier use for it

Batch auditing needs a URL list. LightAudit will crawl a site itself (sitemap plus a robots-aware, same-origin crawl with depth, max-page and exclude-path controls), and for most sites that's all you need.

But when you want to audit someone else's site — a competitor teardown, a pre-pitch agency audit, a JS-heavy SPA that doesn't publish a usable sitemap — run CrawlForge's map_site yourself, from your own MCP client (this one is you driving it, not LightAudit's agent, which is why map_site isn't on its allowlist above), filter the URLs down to the templates that matter (one product page, not four hundred), and paste that list into a LightAudit batch. stealth_mode handles the sites that would otherwise return 403 to a plain crawler.

Two tools, one pipeline: CrawlForge decides what to audit, LightAudit produces numbers you can defend about it.


Why it's free

Because the marginal cost of an audit on your own machine is zero.

There's no account, no licence, no seat, no usage cap, and no upsell hiding behind the third audit. Unlimited local audits and crawls, history, compare, trends, batches, scheduled runs, PSI + CrUX, and AI analysis — all of it, $0, open source under MIT.

What you bring: Node.js 24+, Google Chrome, and optionally your own free Google API key for PSI and your own model for the AI analysis.

What it never does: phone home, store or forward any of your keys, bundle or advertise third-party software, ask you to create an account, or expose itself beyond localhost unless you explicitly say so.


Getting it

It's a plain Next.js app you run yourself — clone, npm install, npm start. First start builds once (about a minute), later starts come up in about a second, bound to loopback only. There's a headless path too, if you'd rather stay in the terminal:

npm run audit -- https://example.com
Enter fullscreen mode Exit fullscreen mode

The repository goes public shortly. If you want the link the moment it does, leave your email at lightauditscore.com — you'll get one email with the repo and the quick start, and then nothing else. No newsletter.

If you'd rather kick the tyres on the research side first, CrawlForge's free 1,000 credits are at crawlforge.dev and work with any MCP client, not just this one.


What would you audit first? I'm genuinely curious whether people want this pointed at localhost during development, at client sites in bulk, or at a scheduled run that quietly watches production overnight. Tell me in the comments — it's shaping what gets built next.

Top comments (0)