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
-
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.jsonExpected 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. -
Verify the throttling is active by reading the metric from each report:
npx lhci openExpected output: the report viewer shows the emulated form factor and CPU/network throttling under "Runtime settings" — confirm
4x slowdownon the mobile run andNo 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.