DEV Community

Cover image for How to Set Up Performance Budgets in CI/CD Pipelines
Apogee Watcher
Apogee Watcher

Posted on • Originally published at apogeewatcher.com

How to Set Up Performance Budgets in CI/CD Pipelines

A performance budget in production is a line you refuse to cross. In CI it is the same line, enforced before a merge or deploy lands. Done well, the pipeline fails fast when a change regresses Core Web Vitals proxies, bundle weight, or your own custom thresholds, so you fix it in the branch instead of shipping first.

This guide assumes you already know why budgets matter. For the full conceptual frame and metric tables, read The Complete Guide to Performance Budgets for Web Teams. Here we focus on wiring budgets into CI/CD: tools, config shapes, and operational pitfalls.

What “performance budget in CI” usually means

In pipelines, teams typically enforce lab metrics (Lighthouse scores and timings such as LCP, CLS, INP where available, TBT, and so on) against numeric ceilings or floors; resource budgets (caps on JS, CSS, image bytes, request counts, or third-party weight); or custom checks (synthetic steps that call your own scripts or APIs after a build).

Lighthouse CI is the common open path for lab metrics because it runs Lighthouse in a controlled environment, stores results, and supports assertions against budgets. Pair it with bundle analysers or size limits when regressions come from dependency drift rather than layout alone.

CI budgets are not a substitute for field data or scheduled monitoring across many URLs. They gate the build you are about to ship. Products such as Apogee Watcher focus on ongoing lab schedules, portfolios, and alerts; see How to Set Up Automated PageSpeed Monitoring for Multiple Sites for that workflow.

Before you write YAML: pick URLs and environments

CI runs should be deterministic enough to trust. Preview URLs from Netlify, Vercel, Cloudflare Pages, or internal preview hosts work if the pipeline waits until the deploy is reachable. Local static servers (serve, http-server) suit static sites; SPAs often need the production build and a correct BASE_URL. Auth walls break Lighthouse unless you script login or use a dedicated test route.

Document which URLs represent home, a heavy template, and checkout or app shell if those differ. One URL with a loose budget hides regressions on another; at minimum, list primary templates in lighthouserc or equivalent.

Path 1: Lighthouse CI (LHCI)

Install

Add Lighthouse CI to the repo (dev dependency is typical):

npm install --save-dev @lhci/cli

Enter fullscreen mode Exit fullscreen mode

Minimal lighthouserc (assertions + collect)

LHCI reads configuration from lighthouserc.js or lighthouserc.json. Example JSON shape:

{
  "ci": {
    "collect": {
      "url": [
        "https://your-preview.example.com/",
        "https://your-preview.example.com/pricing"
      ],
      "numberOfRuns": 3
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["warn", { "maxNumericValue": 300 }]
      }
    },
    "upload": {
      "target": "temporary-public-storage"
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Notes:

  • numberOfRuns: multiple runs reduce noise; three is a common starting point.
  • Assertion keys map to Lighthouse audit IDs; align numeric budgets with your performance budget template and team policy.
  • upload.target: temporary-public-storage is fine for getting started; teams often move to LHCI server or skip upload in pure gate mode.

Wire the CI job

Invoke LHCI after the app is built and the target URL responds. Typical flow:

  1. Install dependencies.
  2. Build the site (if needed).
  3. Deploy to preview or start a static server in the background.
  4. Wait until the test URLs return HTTP 200.
  5. Run lhci autorun (or lhci collect then lhci assert).

If you use GitHub Actions, a dedicated job with timeout-minutes and a health-check step avoids flaky “site not ready” failures. A minimal pattern is to probe the preview URL before lhci autorun, for example with curl -fsS --retry 5 --retry-delay 5 --retry-connrefused "$PREVIEW_URL". --retry-connrefused matters because a deploy that is not listening yet often returns “connection refused”, which plain curl retries do not treat as transient by default. Store the same base URL in a CI variable and pass it into lighthouserc or environment overrides your setup supports, so you do not duplicate hostnames in three places.

Resource limits: budgetsFile vs assertions

Lighthouse CI can assert against a budget.json-style file via assert.budgetsFile (path relative to the working directory). In the upstream configuration model, that budgetsFile mode is an alternative to filling assert.assertions with audit IDs; it is not mixed with other assert options in the same way. Check the Lighthouse CI configuration reference for the exact rules your CLI version supports.

If you want lab metrics (LCP, CLS, and so on) and transfer or request budgets in one assertions block, use Lighthouse CI’s resource-summary assertions (for example resource-summary:script:size, resource-summary:third-party:count) alongside the audit keys. Sizes there use bytes in assertion options; the standalone budget.json format often documents kilobytes, so keep units straight when you copy numbers between files.

Whether you use a checked-in budget file or assertion rows, treat the file like any other policy: generate from your design system pipeline or review diffs in PRs so “max JS kilobytes” does not drift silently.

Path 2: GitHub Actions sketch

Below is a pattern, not a drop-in for every stack: replace build commands, Node version, and URL discovery with your own.

name: Lighthouse CI
on:
  pull_request:
    branches: [main, develop]

jobs:
  lhci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      # Example: wait for preview deploy via your provider’s CLI or API, then:
      - run: npx @lhci/cli autorun
        # Optional: set LHCI_GITHUB_APP_TOKEN if you use the Lighthouse CI GitHub App
        # so upload can post PR status checks. Not required for a local pass/fail gate.
        # env:
        #   LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

Enter fullscreen mode Exit fullscreen mode

Many teams split build and LHCI across workflows so preview deploy completes first; use workflow_run or provider webhooks if needed. The critical invariant is that Lighthouse runs against the same artifact users will get, not a half-built tree.

On GitLab CI, CircleCI, or Buildkite the same steps apply: install Node, build, wait for URLs, then run npx @lhci/cli autorun (or your package script). Cache node_modules between runs when your runner allows it; cold installs dominate wall time on small changes.

Resource budgets alongside Lighthouse

Lab metrics can pass while JavaScript weight creeps up on every sprint. Add at least one of: bundle size limits (bundlesize, size-limit, or webpack’s performance hints), or dependency reviews in the PR template so someone notices a multi-megabyte new dependency.

Treat resource budgets as complementary to LCP and CLS gates. A slow-LCP fix might add twenty kilobytes and still help users; a green Lighthouse run with a nine-hundred-kilobyte main bundle remains fragile.

Flakiness and false positives

CI environments are colder than a developer laptop. Variance in LCP and CLS is normal: mitigate with multiple runs, pinned Chrome, and stable network throttling settings in LHCI. Third-party ads or A/B scripts can differ run to run; block known domains in a test profile or point at a clean route. A cold CDN edge on the first request after deploy can skew timings; an optional warmup GET before LHCI helps.

If the main branch is red every other day, teams stop trusting the job. Prefer warnings on noisy metrics and errors on stable ones until baselines settle.

How this pairs with product-side budgets and alerts

CI answers whether this change broke your thresholds on the URLs you chose. Scheduled monitoring answers whether you are still inside budget next week across the pages you track in production.

If you already use performance budgets and email alerts in Apogee Watcher, treat CI as the pre-merge gate and Watcher as the continuous check on real site inventories. Same vocabulary, different phase of the lifecycle.

Apogee Watcher and CI: agency API (in active development)

You do not need a vendor API to get value from Watcher next to LHCI. Many teams keep Lighthouse CI (or Lighthouse CLI) in the pipeline for fast feedback on the preview URL, and keep budgets, schedules, and digests in Apogee Watcher so production-facing URLs and client reporting stay in one product. Align the numbers: copy thresholds from your performance budget template into both LHCI assertions and Watcher site budgets so “green in CI” and “green in monitoring” mean the same thing.

Apogee Watcher is building a plan-gated customer HTTP API (under /api/v1) aimed at agency workflows. It is in active development: behaviour, routes, and reference documentation will firm up as releases land. Eligible plans will expose the capabilities below; exact rollout timing will follow release notes.

For agency users, the API will be supporting:

  • Check test results: read latest (and historical) PageSpeed outcomes for monitored pages without opening the dashboard.
  • Trigger a new test: request a fresh run after a deploy so CI or a script can wait on Watcher instead of calling the Google PageSpeed API key from your own workers.
  • Retrieve aggregated reports: pull summary or roll-up reporting suitable for client packs or internal gates.
  • Retrieve historical trends: chart-friendly series so pipelines or internal tools can compare this build against last week, not only the last run.

Why that matters next to LHCI: once those endpoints are live, a pipeline will be able to trigger a test, poll until results land, then fail if metrics breach the same budgets you set in the app. Quota and retries will stay on Watcher’s side, and the failing run will remain tied to stored results and trends for client conversations, not only a line in your CI log.

Until the public reference and stable endpoints are published, keep using LHCI for branch gates and the Watcher UI for schedules and alerts. Follow the Product & Brand category on the blog for product news and API-related updates, and check plan details for API access when you are ready to wire automation. If you are interested in early beta tester access to the agency API, contact us.

Checklist: ship a credible CI budget

  • [ ] Listed representative URLs (not only /).
  • [ ] Chose numeric thresholds aligned with your complete guide or client contract.
  • [ ] Set numberOfRuns ≥ 2 for stability.
  • [ ] Documented preview URL or static server startup in the workflow.
  • [ ] Added at least one resource or bundle guard for JS/CSS creep.
  • [ ] Separated error vs warn assertions to reduce alert fatigue.

FAQ

Is Lighthouse CI the only option?

No. Some teams wrap plain Lighthouse CLI, use Playwright traces, or rely on vendor-specific speed tools in CI. LHCI is widely documented and gives assertions + history-friendly uploads out of the box.

Should performance CI block every PR?

Often yes for main, with optional paths for docs-only changes. Use path filters so markdown edits do not run full LHCI unless you want them to.

Can I enforce budgets without a preview deploy?

Yes for static sites: build, serve locally in CI, and point LHCI at localhost. Dynamic server-rendered apps may need a test server with seed data.

Does this replace RUM or Search Console field data?

No. Lab CI validates the candidate build; field metrics validate real users. Both belong in a mature performance program.

What if our budgets are stricter than Lighthouse in CI can reliably hit?

Loosen CI thresholds slightly below production targets, or scope strict checks to stable audits (CLS, bundle size) and use warnings for high-variance metrics until your environment is stable.

Can I trigger Apogee Watcher tests from CI with an API?

That workflow is in active development. The customer API will support checking test results, triggering new tests, retrieving aggregated reports, and retrieving historical trends from automation for agency users on eligible plans (subject to published docs and plan limits). It is not a stable, copy-paste integration yet. For today’s deploy gates, use LHCI or Lighthouse in CI; keep Watcher for scheduled runs, budgets, and alerts. When the reference documentation and endpoints are public, they will be the supported way to align CI with dashboard budgets without putting a PageSpeed API key in your pipeline.

Top comments (0)