Separate Mobile and Desktop Lighthouse Budgets

Running Lighthouse with one config and asserting one set of thresholds forces a choice between a budget that is unrealistic on desktop and one that is too lax for mobile. The fix is two lighthouserc files in the same repository, each with its own throttling profile and assertion block, fanned through a CI matrix. This guide, part of the Mobile vs Desktop Budget Divergence reference, gives the concrete two-config setup, the exact assertion JSON for each form factor, and the matrix job that gates both.

The two configs are structurally identical and differ only in preset, the throttling block, and a handful of numeric ceilings. Keeping them side by side makes the divergence reviewable in a single diff.

Threshold Table Per Form Factor

The table below is the per-form-factor budget these configs encode, calibrated for the P75 user on each profile. Timings loosen on mobile because the network and CPU are throttled; the script byte ceiling tightens because each parsed byte costs more main-thread time on a 4x-throttled CPU.

Assertion Mobile (4G, 4x CPU) Desktop (Cable, 1x CPU)
metric-lcp 2500 ms 2000 ms
metric-inp 200 ms 200 ms
metric-cls 0.1 0.1
total-blocking-time 200 ms 150 ms
resource-summary:script:size 150000 B 250000 B

Diagnostic Steps

  1. Confirm each profile produces distinct timings by running both configs against the same URL.

    npx lhci collect --config=./lighthouserc-mobile.json
    npx lhci collect --config=./lighthouserc-desktop.json

    Expected output: two .lighthouseci/ report sets; the mobile LCP should be meaningfully higher than desktop for the same page. If they match, the mobile throttling is not being applied.

  2. Verify the throttling is active by reading the metric from each report:

    npx lhci open

    Expected output: the report viewer shows the emulated form factor and CPU/network throttling under "Runtime settings" — confirm 4x slowdown on the mobile run and No throttling (CPU) on desktop.

Implementation

Create both files at the repository root. lighthouserc-mobile.json:

{
  "ci": {
    "collect": {
      "url": ["https://staging.example.com/"],
      "numberOfRuns": 5,
      "settings": {
        "preset": "mobile",
        "throttlingMethod": "simulate",
        "throttling": { "cpuSlowdownMultiplier": 4, "rttMs": 150, "throughputKbps": 1600 }
      }
    },
    "assert": {
      "assertions": {
        "metric-lcp": ["error", { "maxNumericValue": 2500 }],
        "metric-inp": ["error", { "maxNumericValue": 200 }],
        "metric-cls": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["error", { "maxNumericValue": 200 }],
        "resource-summary:script:size": ["error", { "maxNumericValue": 150000 }]
      }
    },
    "upload": { "target": "temporary-public-storage" }
  }
}

lighthouserc-desktop.json:

{
  "ci": {
    "collect": {
      "url": ["https://staging.example.com/"],
      "numberOfRuns": 3,
      "settings": {
        "preset": "desktop",
        "throttlingMethod": "simulate",
        "throttling": { "cpuSlowdownMultiplier": 1, "rttMs": 40, "throughputKbps": 10000 }
      }
    },
    "assert": {
      "assertions": {
        "metric-lcp": ["error", { "maxNumericValue": 2000 }],
        "metric-inp": ["error", { "maxNumericValue": 200 }],
        "metric-cls": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["warn", { "maxNumericValue": 150 }],
        "resource-summary:script:size": ["warn", { "maxNumericValue": 250000 }]
      }
    },
    "upload": { "target": "temporary-public-storage" }
  }
}

The mobile config uses numberOfRuns: 5 because 4x throttling amplifies single-sample noise, and gates everything as error; desktop runs three times and treats the looser byte and TBT limits as warn so minor desktop swings do not block merges.

CI Gating Assertion

This matrix job runs both configs in parallel and surfaces a status check per form factor. The exact assertion blocks above are what fail each job.

name: Lighthouse Budget Gate
on:
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        device: [mobile, desktop]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm run build
      - name: Lighthouse CI (${{ matrix.device }})
        run: npx lhci autorun --config=./lighthouserc-${{ matrix.device }}.json

fail-fast: false keeps the desktop job running even when mobile fails, so one run reports both verdicts. Require both lighthouse (mobile) and lighthouse (desktop) checks in branch protection.

Verification

Run both configs locally and confirm each enforces its own ceiling:

npx lhci autorun --config=./lighthouserc-mobile.json
npx lhci autorun --config=./lighthouserc-desktop.json

Each prints an assertion summary; a passing mobile run shows metric-lcp under 2500 ms and a passing desktop run shows it under 2000 ms. Temporarily lower the mobile metric-lcp ceiling to 1000 and re-run to confirm the gate exits non-zero on that config only — that proves the two budgets are independent. For the CPU-multiplier calibration that makes these throttling numbers match your runner, see Device & Network Emulation Weighting.

Frequently Asked Questions

Can I keep both budgets in one lighthouserc file instead of two?

A single config applies one set of collection settings and one assertion block per run, so it cannot encode two different throttling profiles and two threshold sets cleanly. Two files fanned through a matrix keep each form factor's settings explicit and make the divergence reviewable in one diff. See Mobile vs Desktop Budget Divergence for the full rationale.

Why does the mobile config run more times than desktop?

The 4x CPU slowdown on the mobile profile amplifies single-sample variance, so five runs are needed for a stable median; the unthrottled desktop profile is quieter and three runs suffice. Lighthouse keeps the median report, so odd counts avoid tie-breaking.