lighthouse-actions
v1.0.1
Published
Turn a Lighthouse Result (LHR) into a short, ranked list of plain-English things to do next.
Downloads
494
Maintainers
Readme
lighthouse-actions
A dependency-free TypeScript library that accepts a Lighthouse Result object (LHR) and returns a short, ranked list of plain-English things to do next.
import { prioritize } from 'lighthouse-actions';
const actions = prioritize(lhr, { limit: 5 });
// [
// "[performance] Convert your 12 largest images to WebP or AVIF to save about 1.4 MB.",
// "[performance] Defer the 3 render-blocking files in your <head> to cut about 900 ms off first paint.",
// "[accessibility] Add alt text to the 8 images that are missing it.",
// ...
// ]See design-doc.md for the full design rationale — this
README covers what's implemented, how to use it, and how it was validated.
Install
bun add lighthouse-actionsESM only, zero runtime dependencies, Node >= 20. Also works in browsers and edge workers.
API
import { prioritize, analyze } from 'lighthouse-actions';
prioritize(lhr, options?): string[]
analyze(lhr, options?): Analysisprioritize is analyze(lhr, options).actions.map(a => a.sentence). Use
analyze when you need more than strings — category, priority, effort,
impact numbers, and evidence per action.
Subpath exports lighthouse-actions/score and lighthouse-actions/render
expose the priority model and the sentence registry directly, for callers
who want to swap a piece out.
Options
| Option | Default | Notes |
|---|---|---|
| limit | 10 | Max actions returned |
| categories | all present | Restrict to these category ids |
| categoryWeights | { seo: 1.2, accessibility: 1.1, performance: 1.0, 'best-practices': 0.9 } | Multiplier per category. SEO outranks performance by default — see "Category weights" below |
| minPriority | 0 | Drop actions below this priority |
| includeInformative | false | Include informative audits (e.g. third-party-summary) |
| voice | 'plain' | 'technical' allows LCP/CLS/TBT/FCP jargon |
| effort | — | Override/extend the built-in effort table |
| unknownAudits | 'generic' | 'skip' drops audits the registry doesn't recognize instead of using a generic sentence |
| useStackPacks | false | Preview: prefer lhr.stackPacks framework-specific advice over the built-in template when available |
Architecture
Six pure stages (src/index.ts wires them together):
unknown → normalize → filter → score → render → select → Analysis- normalize (
src/normalize/) — one adapter per Lighthouse major (v10–v13, plus a pre-v10legacyadapter), dispatched bylighthouseVersion, all converging on a version-agnosticFindingmodel. - filter (
src/filter.ts) — drops passing,notApplicable,manual,error, and (by default)informativeaudits, and restricts tooptions.categories. - score (
src/score/priority.ts,src/score/effort.ts) — the priority model from design-doc.md §7:scoreGain × impactBoost ÷ effortCost. - render (
src/render/) — a hand-written sentence per audit id where one exists (src/render/templates/*.ts), a generic fallback otherwise (src/render/fallback.ts), and an optional stack-pack override. - select — dedupe, apply
limitandminPriority(insrc/index.ts).
Category weights (§12.1)
Default weights are { seo: 1.2, accessibility: 1.1, performance: 1.0,
'best-practices': 0.9 } — SEO outranks performance by default, per the
project's resolved open question. Override via options.categoryWeights if
your context (e.g. an internal tool, not a public site) disagrees.
Effort table and priority sanity checks (§12.2)
src/score/effort.ts is curated judgment (src/data/known-audits.ts lists
every audit id this repo has tested against; anything absent from the
effort table falls back to 'unknown', which sits just above 'low' cost so
unfamiliar audits neither dominate nor vanish).
Per §12.2, that table was meant to be evaluated against real reports for
wikipedia.org, godaddy.com, slack.com, and ngdomain.ng. This sandbox has no
outbound path to run that evaluation for real: there's no Chrome available
to run Lighthouse locally, and the PageSpeed Insights API (the one
network-only route to a real LHR) returns HTTP 429 with a zero daily quota
for anonymous callers on this network — see scripts/gen-fixtures.ts for the
exact failure. Run bun scripts/gen-fixtures.ts yourself in an
environment with a PSI API key or local Chrome to regenerate test/fixtures/
from real reports; the loader (test/helpers/load-fixture.ts) doesn't care
where the JSON came from.
In lieu of that, test/fixtures/{wikipedia,godaddy,slack,ngdomain,justiceo,
amazon,jiji,jumia}.json are synthesized from each site's well-known
characteristics (lean and fast for Wikipedia; heavy marketing images and
third-party tags for GoDaddy; JS-bundle-heavy for Slack; unoptimized and
pre-HTTPS-hardened for a smaller regional registrar; a lean personal site for
justiceo.com; heavy third-party/ad tags at scale for Amazon; unoptimized
classifieds imagery and slow backend for jiji.ng; a regional e-commerce mix
of image weight, JS, and a known-vulnerable library for jumia.com.ng) using a
shared audit catalog (scripts/fixture-catalog.ts) that mirrors real
Lighthouse v10–13 audit ids and weights. Run bun
scripts/inspect-fixtures.ts to print the top-5 for every fixture; the
current output (also exercised by test/golden.test.ts) reads as a reviewer
would expect — quick image/markup fixes outrank multi-week rewrites, and
outright failures outrank near-misses. Treat the absolute ranking as a
plausibility check, not a validated ground truth, until someone re-runs it
against real reports.
How to validate against real data
scripts/inspect-fixtures.ts prints, per action, priority = gain x
impact / effort — the exact terms from §7 — so a reviewer can see why
something ranked where it did instead of cross-referencing priority.ts/
effort.ts by hand. To actually validate the model:
- Run
bun scripts/gen-fixtures.tsin an environment with a PSI API key or local Chrome, pointed at wikipedia.org, godaddy.com, slack.com, ngdomain.ng, justiceo.com, amazon.com, jiji.ng, jumia.com.ng, and any other site whose profile isn't already represented. - Replace the corresponding synthesized fixtures in
test/fixtures/with the real captures. - Run
bun run sync-audits(new real audit ids may need a template or anIGNOREDentry — see "Coverage" below) thenbun scripts/inspect-fixtures.ts. - Read the top-5 breakdown for each site. If an ordering feels wrong, the
breakdown tells you whether to adjust
src/score/effort.ts(a specific audit's cost) orDEFAULT_CATEGORY_WEIGHTSinsrc/types.ts(a category-wide multiplier) — argue about one line, not the model.
Stack packs (§12.4, preview)
options.useStackPacks: true prefers lhr.stackPacks[].descriptions[auditId]
over the built-in template when the LHR carries one, so a Next.js site gets
"Use next/image..." instead of the generic form. Off by default: that text
isn't ours to length- or tone-check.
Third-party attribution (§12.3)
third-party-summary is informative (no score) and excluded by default.
With includeInformative: true, it's attributed to the top entity from
lhr.entities/the audit's items — "Reduce or defer Google Tag Manager; it's
costing you about 450 ms of main-thread time." — instead of a generic
sentence.
Coverage (§9)
bun run sync-audits walks every committed fixture, writes the union of
audit ids to src/data/known-audits.ts, and test/coverage.test.ts asserts
every one is either in the registry or on the IGNORED list
(src/data/ignored-audits.ts) with a reason. Adding a fixture from a new
Lighthouse version therefore fails CI with exactly the ids that need
attention.
89 audit ids are known, and all 89 have a hand-written template (100%,
against the v1.0 target of 90%). The kitchen-sink fixture exists solely to
exercise the ~46 ids that have a template but never appeared in a real or
scenario-modeled fixture (things like canonical, tabindex,
geolocation-on-start) — its catalog entries carry weight 0 so they can't
perturb ranking in the other fixtures; see scripts/fixture-catalog.ts's
COVERAGE_EXPANSION for the list and rationale.
Testing
bun testtest/priority.test.ts— the priority model's worked example from §7.test/normalize.test.ts— version-drift adapters (v10–v13, legacy).test/render.test.ts— voice, fallback, stack packs, third-party attribution.test/sentence-rules.test.ts— the six sentence rules from §8, over every fixture.test/coverage.test.ts— the §9 coverage backstop.test/golden.test.ts— committed.expected.txtper fixture; regenerate withbun run gen-goldenafter an intentional model/template change and review the diff as English sentences.test/analyze.test.ts— robustness (garbage input,runtimeError, single-category LHRs) and the invariants from §10 (length ≤ limit, unique sentences, non-increasing priority, deterministic output).
CLI companion (§11)
bunx @lighthouse-actions/cli report.json --limit 5Lives in packages/cli, depends on the root package, and stays out of the
core package's dependency graph. --json prints the full Analysis object;
--category/--voice mirror the library options. Every sentence already
carries a [category] tag straight from the library (§8.1 — Add a meta
description... comes back as [seo] Add a meta description...); pass
--hide-category for a bare sentence. --group-by category prints output
under a ## <category> heading per group instead (and drops the
now-redundant per-line tag); --show-effort extends the tag with
low/medium/high, e.g. [seo, low]. Plain-text output also prints a
trailing ...and N more
(raise --limit to see them). line whenever Analysis.total exceeds what
was shown, so a truncated CI comment doesn't read as a complete list.
Packaging
ESM only. bun build bundles the JS (see scripts/build.ts); bun run
build runs that plus tsc -p tsconfig.build.json for .d.ts emission,
since Bun's bundler has no declaration-emit equivalent. Zero runtime
dependencies, sideEffects: false. Subpath exports: lighthouse-actions,
lighthouse-actions/score, lighthouse-actions/render.
Milestone status
| | Scope | Status |
|---|---|---|
| v0.1 | Normalize (v13), priority model, ~30 performance templates, prioritize + analyze | Done |
| v0.2 | Accessibility/SEO/best-practices templates, effort table, coverage test | Done |
| v0.3 | v10–v12 adapters, full options surface, stack packs preview | Done |
| v1.0 | Locked API, ≥90% audit coverage across fixtures, docs, CLI companion | Done |
