[ ANALYSIS ][ LIGHTHOUSE ][ PERFORMANCE ][ NEXTJS ][ CI_CD ]

Lighthouse CI — automated Next.js performance audit in GitHub Actions (2026)

May 28, 20269 min read
Author: DevStudio.itWeb & AI Studio

How to block Core Web Vitals regressions in PRs — Lighthouse budgets, lighthouserc.json, GitHub Actions workflow, and the GA4 vs Google Ads tradeoff.

READ_TIME: 9 MIN_COMPLEXITY: MED_
STAMP: VERIFIED_BY_DS_

TL;DR

Lighthouse CI (LHCI) runs a performance audit on every pull request and blocks merge when LCP exceeds 2.5 s, CLS exceeds 0.1, or Performance score drops below 0.85. For a Next.js 15 project on Node 22 with build node scripts/build.cjs and lint eslint, it is the cheapest insurance against a "small CSS change" that breaks SEO and Google Ads conversions a week later. Below: a working lighthouserc.json, GitHub Actions workflow, and the deliberate tradeoff GA4 afterInteractive vs Ads tags lazyOnload.

Who this is for

  • Next.js / React teams deploying to Vercel or self-hosted
  • Business sites where marketing and Ads depend on fast LCP
  • Companies without dedicated performance QA — LHCI is a bot that does not forget mobile
  • Developers tired of manual Lighthouse once a quarter "when quality happens to be OK"

Keywords (SEO)

lighthouse ci nextjs, automated performance audit, github actions lighthouse, core web vitals ci, lighthouserc.json, performance regression pull request

Why a one-off audit is not enough

Every PR can introduce:

  • a new analytics script without strategy="lazyOnload",
  • a hero image without width/heightCLS spike,
  • importing a whole icon library instead of tree-shaking,
  • a font without display: swap,
  • a client component heavier than the previous Server Component.

Lighthouse on next dev locally often lies — different bundling, no production optimizations, different cache. CI on next build + next start artifact is closer to what users and Google see.

Reference stack (DevStudio / Next.js 15)

Element Value
Framework Next.js 15.5.x
Node 22.x (engines in package.json)
Build npm run buildnode scripts/build.cjs
Lint npm run linteslint
Production start npm run startnext start
Analytics GA4 afterInteractive, Google Ads lazyOnload

This tag split matters in LHCI: a PR adding another afterInteractive script may pass functionally but drop Performance — exactly what the pipeline should catch.

Thresholds worth enforcing (mobile)

Metric LHCI threshold Why
LCP 2500 ms Hero, first impression, SEO
CLS 0.1 Jumping form = fewer leads
INP 200 ms (optional in assert) Menu, chatbot, interactions
Performance score 0.85 Landing page quality signal (Ads, Search)

Thresholds are deliberately strict for a marketing site — a SaaS dashboard app may use different budgets.

lighthouserc.json — working example

Place in the repository root:

{
  "ci": {
    "collect": {
      "numberOfRuns": 3,
      "startServerCommand": "npm run start",
      "startServerReadyPattern": "Ready",
      "startServerReadyTimeout": 120000,
      "url": [
        "http://localhost:3000/pl",
        "http://localhost:3000/en",
        "http://localhost:3000/pl/strony-www"
      ],
      "settings": {
        "preset": "desktop",
        "onlyCategories": ["performance", "accessibility", "best-practices", "seo"]
      }
    },
    "assert": {
      "preset": "lighthouse:recommended",
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.85 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "interactive": ["warn", { "maxNumericValue": 3800 }],
        "total-blocking-time": ["warn", { "maxNumericValue": 300 }]
      }
    },
    "upload": {
      "target": "temporary-public-storage"
    }
  }
}

Practical notes:

  • numberOfRuns: 3 — median of three runs reduces LHCI noise.
  • Pick business-critical URLs (home PL/EN + key service page).
  • For mobile, change preset to "perf" or add a separate job with emulatedFormFactor: "mobile" in settings (separate config file or matrix in Actions).
  • upload.target: temporary-public-storage — quick report links in PR without your own LHCI server (for production consider LHCI Server or GitHub artifacts).

GitHub Actions — full workflow

File .github/workflows/lighthouse.yml:

name: Lighthouse CI

on:
  pull_request:
    branches: [main, master]
  push:
    branches: [main, master]

jobs:
  lhci:
    runs-on: ubuntu-latest
    timeout-minutes: 25

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Build
        run: npm run build
        env:
          # Fill variables required by build.cjs / Next (no secrets in logs)
          NODE_ENV: production

      - name: Run Lighthouse CI
        run: |
          npm install -g @lhci/cli@0.14.x
          lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

Optional separate job on nightly deploy with Vercel Preview URL (closer to CDN):

      - name: Collect against Vercel Preview
        if: github.event_name == 'pull_request'
        run: lhci collect --url="${{ steps.vercel.outputs.preview_url }}/pl"

Preview better reflects edge cache but needs stable URL and secrets — localhost after next build is a good start without extra infrastructure.

Pipeline order — what must run before LHCI

  1. npm ci
  2. npm run lint — fast fail before expensive build
  3. npm run buildsame build as production (scripts/build.cjs)
  4. lhci autorun — start server, 3× URL, assert

Without step 3, LHCI measures the dev server — false green light.

Typical regressions caught in PRs

Marketing scripts

Adding gtag without strategy="lazyOnload" in next/script raises TBT and delays LCP. On the reference site:

  • GA4afterInteractive (do not lose generate_lead on fast submit),
  • Google Ads (AW-...)lazyOnload (protect performance).

A PR moving all tags to afterInteractive may fix Ads but hurt Performance — LHCI should catch it; marketing must know the tradeoff (see the Google Ads conversion tracking article).

Images and fonts

  • Hero: priority, known dimensions, AVIF/WebP where possible.
  • Font: next/font or display: swap.
  • Missing width/height on footer logo → CLS when font loads.

JavaScript

  • Dynamic import for chatbot and heavy widgets.
  • Avoid large bundles in the Server Component layout.

Integration with Vercel and RUM

Layer What it gives
LHCI in PR Synthetic before merge — blocks regression
Vercel Analytics / Speed Insights RUM of real users after deploy
Search Console Core Web Vitals Google field data with delay

LHCI does not replace RUM — use both. LHCI is a quality gate; RUM is truth in the field (weak devices, 3G, adblock).

Bot comment on PR

With LHCI_GITHUB_APP_TOKEN (official Lighthouse CI app), reports land in PR comments with PR vs base comparison. Without the token you still have Actions logs and temporary-public-storage.

Extended assert — SEO and accessibility

Besides performance, consider:

"categories:seo": ["warn", { "minScore": 0.9 }],
"categories:accessibility": ["warn", { "minScore": 0.9 }],
"categories:best-practices": ["warn", { "minScore": 0.9 }]

Start with warn, then error once stable. Otherwise every PR with a minor axe rule goes red and the team disables LHCI.

CI time cost

Estimate: 3 runs × 3 URL × ~40–60 s6–10 minutes + Next build (2–8 min). Speed-ups:

  • cache ~/.npm and .next/cache in Actions,
  • fewer URLs in collect (only /pl on feature branches),
  • full URL set only on main.

Still cheaper than a week of conversion drop after LCP regression.

FAQ

Does Lighthouse CI replace manual PageSpeed Insights?

No — PSI is still useful ad hoc. LHCI automates the same on every PR. Keep PSI on the production URL as a post-release check.

Can I test desktop only?

Yes, but Google uses mobile for CWV in Search. Minimum: one mobile job on main, desktop on PR if CI time hurts.

Build needs environment variables — what in CI?

Duplicate non-secret variables in job env:; secrets in GitHub Secrets. build.cjs often loads the same as Vercel — without that, build fails before LHCI.

PR adds GA4 on afterInteractive — is that wrong?

Not always — it is a product decision (do not lose leads). LHCI may show Performance regression — then optimize elsewhere (images, fonts, code-split), not necessarily revert GA4.

Should eslint run in the same workflow as LHCI?

Yes — fast fail. A lint-only workflow is also fine; what matters is production build as a gate before lhci autorun.

Local run before pushing a PR

On developer machine (Node 22):

npm run build
npm run start
# in second terminal:
npx @lhci/cli autorun

If local passes but CI fails — check environment variables, Node version, and cache. If local fails but Vercel is OK — you are probably measuring dev instead of production build.

One-day rollout checklist

  • lighthouserc.json in repo with LCP / CLS / Performance thresholds
  • Workflow with Node 22, npm ci, lint, build, lhci autorun
  • 2–3 URLs critical for conversion
  • Secret LHCI_GITHUB_APP_TOKEN (optional comments)
  • Team knows: GA4 early, Ads late — not "everything lazyOnload"
  • After merge: RUM on production for verification

Example mobile job (separate matrix)

In the same workflow add a second step with mobile preset — critical for Google CWV:

      - name: Run Lighthouse CI (mobile)
        run: |
          npm install -g @lhci/cli@0.14.x
          lhci autorun --config=lighthouserc.mobile.json

File lighthouserc.mobile.json — copy of base config with:

"settings": {
  "preset": "perf",
  "formFactor": "mobile",
  "screenEmulation": { "mobile": true }
}

Keep LCP/CLS asserts the same — mobile is stricter, so passing mobile in CI usually means desktop is safe too.

What to do when LHCI fails after merge

  1. Open report from temporary-public-storage or bot comment.
  2. Check opportunities (images, unused JS, render-blocking).
  3. Compare diff with base branch — which PR introduced regression.
  4. Do not lower thresholds permanently without business reason — better a one-time exception with a ticket than permanent minScore: 0.7.

Typical win: move script to lazyOnload, priority on LCP image, dynamic(() => import(...)) for chatbot.

Google evaluates landing page experience partly through speed and layout stability. An LCP regression in a merged PR can raise CPC and lower Quality Score — cost visible in Ads, not only Lighthouse. The team adding AW- tags should see red LHCI before the campaign learns on a slower page.

scripts/build.cjs — why not raw next build

In production projects, build often wraps env validation, Prisma, asset copy, or steps before next build. LHCI must use the identical command as Vercel (npm run build), otherwise you measure a different bundle than users. If build fails in CI for missing DATABASE_URL, set a mock or skip DB only in the LHCI job — but document it in README for the team.

Summary

Lighthouse CI on Next.js 15 is the rule of the game: lint → production build → audit → assert. Thresholds LCP ≤ 2.5 s, CLS ≤ 0.1, Performance ≥ 0.85 protect SEO and landing page quality under Ads. Remember analytics tradeoff: GA4 afterInteractive vs Google Ads lazyOnload — the pipeline should show the cost of PRs that break that balance. Synthetic CI + RUM after deploy = full picture. Node 22, eslint, and scripts/build.cjs are the same quality chain production sees.

Want a performance pipeline for your project?

About the author

We build fast websites, web/mobile apps, AI chatbots and hosting setups — with a focus on SEO and conversion.

Recommended links

From theory to production — Branchly, our hosting stack, care plans and shipped work.

LIKE HOW WE THINK? LET'S BUILD SOMETHING TOGETHER.

[ START_PROJECT_CONFIGURATION ]