Skip to content
brainNotFound

Build Systems/Rendering

Enforce a performance budget in CI

Fail the pull request, not the production deploy — a Lighthouse CI gate on three representative URLs.

intermediate25 min

// read first

A performance budget nobody enforces is a note in a document. Put it in CI and it becomes a property of the codebase.

Three URLs is the right number: a marketing page, a long article, and the heaviest template you ship. Adding more slows the gate without finding new classes of regression.

Add the gate

  1. Step 01

    Write the budget down

    Assertions live in a config file next to the workflow, so changing the budget shows up in review as a diff rather than a discussion.

    lighthouserc.jsonjson
    {
      "ci": {
        "collect": {
          "url": [
            "http://localhost:3000/",
            "http://localhost:3000/blog/seo-nextjs-app-router",
            "http://localhost:3000/docs/server-setups/provision-ubuntu-node-server"
          ],
          "numberOfRuns": 3,
          "settings": { "preset": "desktop" }
        },
        "assert": {
          "assertions": {
            "categories:performance": ["error", { "minScore": 0.9 }],
            "categories:seo": ["error", { "minScore": 1 }],
            "categories:accessibility": ["error", { "minScore": 0.95 }],
            "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
            "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
          }
        }
      }
    }
  2. Step 02

    Run it against a production build

    Never audit the dev server. It ships unminified bundles and no caching, so the numbers are meaningless and the gate will either always fail or be set so loose it never fires.

    .github/workflows/lighthouse.ymlyaml
    - run: npm ci
    - run: npm run build
    - run: npx --yes @lhci/cli autorun --config=lighthouserc.json
  3. Step 03

    Median of three runs, not one

    A single Lighthouse run on shared CI hardware varies by ten points or more. Three runs with the median taken is the smallest number that produces a gate people trust rather than retry.

What to do when it fails

AssertionUsual causeFirst thing to check
LCPHero image lazy or unsizedpriority + explicit width/height
CLSWeb font or late bannernext/font, reserve space
TBTClient component too high in the treePush "use client" to leaves
SEO < 100Missing meta description or canonicalgenerateMetadata on the route
Triage by failing metric

// related

From the rest of the site.