Testland
Browse all skills & agents

lighthouse-budget-author

Drafts a `lighthouserc.js` (or `budget.json`) at design time - picks Web Vitals thresholds (LCP / INP / CLS) per route based on traffic class (cached / dynamic / API-heavy / form-heavy) and the team's NFRs, plus resource-size budgets (JS / CSS / images / total bytes). Emits the config file ready for the lighthouse-perf runner. Use when starting Lighthouse coverage on a project that has no budgets yet, or when the existing budgets need a redesign.

Install with skills.sh (any agent)

npx skills add testland/qa --skill lighthouse-budget-author
View source

lighthouse-budget-author

Overview

lighthouse-perf is the runner - it takes a .lighthouserc.js and runs Lighthouse against it. This skill writes the config, picking thresholds per route at design time so the runner has something meaningful to assert against.

The two artifacts this skill produces:

  1. Lighthouse CI assertion config in .lighthouserc.js - per-route LCP / INP / CLS thresholds.
  2. Resource-size budget in budget.json (the Lighthouse "performance budgets" feature) - per-resource-type byte caps.

Without this skill, teams either (a) set every threshold to the "good" default and ignore route-specific reality, or (b) pick thresholds via guesswork. Either ends with a gate the team disables.

When to use

  • The project just installed @lhci/cli and needs an initial config.
  • An NFR review (per non-functional-requirement-extractor) produced perf NFRs that need to translate into Lighthouse syntax.
  • Existing budgets are uniform across routes (same threshold for every URL); the team wants per-route tightening.
  • The team wants resource-size budgets (JS bundle ≤ 300kb, etc.) in addition to Web Vitals.

Step 1 - Inventory the routes

For each route to be audited:

FieldNotes
URL patternThe actual URL or a representative one.
Traffic classcached / dynamic / api-heavy / form-heavy / media-heavy.
Auth statepublic / logged-in (auth state changes the JS bundle).
Cache TTLStatic assets duration.
User-tier trafficWhat % of traffic does this route account for? Drives strictness.

The URL set should cover the team's high-traffic routes plus any known-slow long-tail page. Don't audit every URL - pick representatives.

Step 2 - Pick LCP / INP / CLS thresholds per route

Start from the canonical Web Vitals "good" thresholds (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1) and adjust per traffic class:

Traffic classLCP targetINP targetCLS targetReasoning
Cached (CDN-served, mostly static)≤1.5s≤100ms≤0.1The default is too lenient; cached pages should be fast.
Dynamic (per-user content)≤2.5s≤200ms≤0.1Default thresholds.
API-heavy (waterfall of fetches)≤3.0s≤200ms≤0.1Acknowledge real-world latency; tighten elsewhere.
Form-heavy (input + validation)≤2.5s≤100ms≤0.05INP matters more - every keystroke is an interaction. CLS strict because forms must not jump.
Media-heavy (images / video)≤2.5s≤200ms≤0.1LCP via priority hints + loading=eager on hero.

For a team's first pass, start lenient (Web Vitals defaults across the board) and tighten one route at a time as evidence accumulates that the route is consistently faster than the default.

Step 3 - Pick resource-size budgets

A resource-size budget caps the total bytes per resource type. The canonical Lighthouse "good" defaults for a non-media-heavy public page:

Resource typeSuggested budgetNotes
script300 kbCompressed JS bundle (gzip / brotli). Tighten to 150kb for marketing pages.
stylesheet100 kbCSS only. Most projects fit easily.
image500 kbPer page - adjust upward for media-heavy.
font100 kbSubset fonts; use variable fonts when possible.
total1500 kbPage total; CDN-cached or not.

For Single-Page Apps where the JS bundle is the load-bearing cost, the JS budget is the most important. Subpaths (route chunks) keep this tractable as the app grows.

Step 4 - Emit lighthouserc.js

A representative config that lives at the project root:

// .lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: [
        'http://localhost:3000/',                      // marketing home (cached)
        'http://localhost:3000/pricing',               // marketing pricing (cached)
        'http://localhost:3000/dashboard',             // logged-in dynamic
        'http://localhost:3000/orders/new',            // form-heavy
      ],
      numberOfRuns: 3,
      settings: {
        preset: 'desktop',
        chromeFlags: '--no-sandbox',
        budgetPath: './budget.json',                  // resource-size budget
      },
      startServerCommand: 'npm run start',
      startServerReadyPattern: 'ready on',
    },
    assert: {
      assertMatrix: [
        // Cached marketing pages - strict
        {
          matchingUrlPattern: '^http://[^/]+/(pricing)?$',
          assertions: {
            'largest-contentful-paint': ['error', { maxNumericValue: 1500 }],
            'interaction-to-next-paint': ['error', { maxNumericValue: 100 }],
            'cumulative-layout-shift':   ['error', { maxNumericValue: 0.1 }],
          },
        },
        // Dynamic logged-in pages - defaults
        {
          matchingUrlPattern: '/dashboard',
          assertions: {
            'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
            'interaction-to-next-paint': ['error', { maxNumericValue: 200 }],
            'cumulative-layout-shift':   ['error', { maxNumericValue: 0.1 }],
          },
        },
        // Form-heavy pages - strict CLS
        {
          matchingUrlPattern: '/orders/new',
          assertions: {
            'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
            'interaction-to-next-paint': ['error', { maxNumericValue: 100 }],
            'cumulative-layout-shift':   ['error', { maxNumericValue: 0.05 }],
          },
        },
      ],
    },
    upload: { target: 'temporary-public-storage' },
  },
};

assertMatrix lets per-route assertions live in one config - easier to review than separate config files.

Step 5 - Emit budget.json

Lighthouse's resource-size budget format:

[
  {
    "path": "/*",
    "resourceSizes": [
      { "resourceType": "script",     "budget": 300 },
      { "resourceType": "stylesheet", "budget": 100 },
      { "resourceType": "image",      "budget": 500 },
      { "resourceType": "font",       "budget": 100 },
      { "resourceType": "total",      "budget": 1500 }
    ],
    "resourceCounts": [
      { "resourceType": "third-party", "budget": 10 }
    ]
  },
  {
    "path": "/marketing/*",
    "resourceSizes": [
      { "resourceType": "script",     "budget": 150 },
      { "resourceType": "stylesheet", "budget": 50 },
      { "resourceType": "total",      "budget": 800 }
    ]
  }
]

Per-path budgets allow stricter targets for marketing pages (where load time directly impacts conversion) than for logged-in app routes.

Step 6 - Validate the budget realistically

Before committing, run Lighthouse against current production with the new config:

LHCI_BUILD_CONTEXT__GITHUB_BASE_URL=... npx lhci collect --url=https://prod.example.com/
npx lhci assert

Expect some failures - that's the point. The budget surfaces what needs work. Categorize the failures:

Failure kindDecision
Single-route outlierFile a perf-improvement ticket; relax the budget on this route only with a TODO.
Universal failureThe budget is too strict; relax to current p75 + 10%.
Budget violated only on mobile presetAdd a separate lighthouserc.mobile.js with looser mobile budgets.

Never set a budget that always passes today - it'll never catch a regression.

Anti-patterns

Anti-patternWhy it failsFix
Same threshold for every routeCached marketing page passes the same gate as a logged-in dashboard.Use assertMatrix with per-route patterns.
Threshold = current production valueNo room for a real regression to be caught.Set threshold = current + 10% headroom; tighten over time.
Budgets in one mega-file with no per-path scopeStrict marketing budget falsely fails the dashboard.Per-path budget objects.
Setting the threshold to the "Good" target on day oneMost production sites don't meet the targets out of the box; team disables the gate.Land the gate at current production levels first; tighten the budget as a separate task.
Skipping the resource-size budgetWeb Vitals catch the symptom (slow LCP); resource-size catches the cause (JS bundle bloat).Both. They're complementary.

References

  • lighthouse-perf - the runner that consumes this skill's output.
  • non-functional-requirement-extractor - upstream skill that produces threshold-bound NFRs translated by this skill into Lighthouse syntax.
  • perf-budget-gate - downstream unified gate that consumes Lighthouse + load-runner verdicts.
  • web.dev/articles/vitals - canonical LCP / INP / CLS thresholds at the 75th percentile.
  • Lighthouse performance budgets - https://web.dev/articles/use-lighthouse-for-performance-budgets

Related skills

db-query-plan-analyzer

Reads `EXPLAIN` / `EXPLAIN ANALYZE` output from PostgreSQL, MySQL, or SQLite - identifies the dominant cost (sequential scan, nested loop, sort spill, missing index, type-cast preventing index use), proposes the specific index or query rewrite to fix it, and emits the candidate `CREATE INDEX` statement. Use when load testing or production telemetry shows the database as the bottleneck and the team needs targeted query-level remediation.

flame-graph-analyzer

Reads CPU flame-graph output from py-spy (Python), async-profiler (JVM), Go pprof, or Node.js `perf_hooks` / clinic.js: identifies the hot path (top sample-time frames), classifies the bottleneck (CPU-bound vs lock contention vs allocator pressure), and proposes the next investigation step. Use when a perf regression is bisected to a commit but the hot path inside it is unclear; for tail-latency percentiles use latency-percentile-analyzer, for GC pauses specifically use jvm-gc-tuning, and for a slow SQL hot path use db-query-plan-analyzer.

gatling-load-testing

Authors Gatling simulations in Java / Kotlin / Scala (or JS / TS) using the Simulation class plus http() / scenario() / exec() DSL builders, ramps virtual users via injectOpen (arrival rate) or injectClosed (concurrent count), runs via Maven / Gradle / sbt with the Gatling plugin, and gates CI on assertions defined in setUp(). Use when the project is on the JVM and the team prefers code-first load tests over JMeter's XML or k6's JavaScript-only authoring.

jmeter-load-testing

Authors Apache JMeter `.jmx` test plans (Thread Groups + HTTP samplers + assertions + listeners) in the JMeter GUI, runs them headlessly via `jmeter -n -t plan.jmx -l results.jtl`, generates an HTML dashboard with `-e -o`, and gates CI on JTL parsing. Use when the project has an existing JMeter investment, needs JVM-native load tooling, or works in domains with strong JMeter community support (banking, telecom, enterprise).

jvm-gc-tuning

Diagnoses JVM garbage-collection behaviour under load: reads and interprets unified GC logs (-Xlog:gc*), selects the right collector (G1 vs ZGC vs Parallel vs Serial), tunes heap sizing and pause-time targets, quantifies allocation rate, and traces the GC-pause-to-latency-tail link using GCViewer and Java Flight Recorder (JFR). Use when a load test reveals p99/p999 latency spikes that correlate with GC activity, or when heap sizing and collector selection need justification before a performance baseline is locked.

k6-load-testing

Authors k6 JavaScript load-test scripts (VU loops + checks + sleeps), configures the `options` block with `stages` (ramp-up patterns) and `thresholds` (p(95) latency, error rate), runs via `k6 run script.js` or `--vus / --duration` ad-hoc flags, and uses thresholds as the CI pass/fail signal. Use when the project ships HTTP / WebSocket / gRPC load tests and the team wants developer-friendly JavaScript authoring.

latency-percentile-analyzer

Interprets latency distributions from k6 load tests beyond the p95/p99 gate: reads percentile summaries and JSON exports to identify tail shape, computes the tail ratio (p99/p50) as a distribution-spread signal, detects bimodal distributions, explains coordinated omission and why naive p99 values are optimistic under sustained load, and distinguishes request-rate from concurrency models. Use when a k6 threshold passes but the system still feels slow, when p99 is suspiciously low during ramp-up, or when the team needs to explain why tail latency is high rather than just observing that it is.

lighthouse-perf

Configures Lighthouse CI (`@lhci/cli`) to audit Web Vitals (LCP, INP, CLS) on every PR, asserts against canonical thresholds (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1 at the 75th percentile), uploads Lighthouse reports as build artifacts, and posts deltas as PR comments. Use when the project ships a web frontend and the team needs continuous Web Vitals monitoring tied to PR gating.

load-testing-overview

Teaches load and performance testing from zero: how to choose between k6, JMeter, Gatling, Locust, and Artillery based on observable project facts (team language, tests-as-code vs GUI authoring, protocols beyond HTTP, CI gating needs); the six load profiles (smoke, average-load, stress, spike, soak, breakpoint) and the question each one answers; the difference between open workload models that hold arrival rate constant and closed models that hold concurrent users constant; why percentiles rather than averages are the unit of measurement; and how to turn a run into a pass/fail CI gate, with a first runnable k6 script. Use when a service needs performance coverage and the tool, the load profile, or the pass/fail threshold has not been decided yet.

locust-load-testing

Authors Locust load tests as Python classes - HttpUser with @task-decorated methods plus on_start hooks and between() wait_time - runs via `locust -f locustfile.py` headless mode (or distributed via `--master` / `--worker`), and exports CSV / JUnit reports for CI gating. Use when the project's primary stack is Python and the team wants load tests in the same language as the application.

perf-budget-gate

Builds a unified release-readiness gate that aggregates verdicts from any combination of k6 / JMeter / Gatling / Locust load runners and Lighthouse CI Web Vitals, applies severity-aware pass/fail thresholds, and emits a single go / no-go decision with per-metric deltas vs the main-branch baseline. Posts the delta as a PR comment when the team has the integration set up. Use when authoring a CI step that gates a deployment on cross-runner perf compatibility.

slo-load-test-plan

Turns a service's SLOs and endpoint traffic mix into a named scenario matrix: one scenario per SLO boundary condition, a load profile (smoke, average-load, stress, soak, spike, breakpoint) per scenario, an open or closed workload injection model, a threshold expression derived from the SLO the scenario guards, and an error-budget calculation that sets the soak run's failure allowance. Stays runner-agnostic and fixes the pass/fail line before any tool is configured. Use when an SLO document and an endpoint list both exist but nobody has decided which load runs to make, what shape of load each carries, or what number would count as a failure.