Mobile vs Desktop Budget Divergence

A single performance budget applied to both form factors fails in both directions: it is too loose to protect the mid-range Android phone on 4G that actually defines your field scores, and too tight to be realistic on a desktop with a fast CPU and fiber. The two environments differ by a factor of four in CPU throughput and an order of magnitude in network latency, so one ceiling cannot be correct for either. This is the device-divergence layer of the Defining Web Performance Budgets reference: it maintains distinct, calibrated thresholds per form factor and enforces both in the same pipeline so a regression on the constrained path is never masked by headroom on the fast one.

Divergence is two coupled commitments — two collection environments (a throttled mobile profile and an unthrottled desktop profile) and two threshold sets (tighter byte budgets and looser timings for mobile, the reverse for desktop). This page is the authoritative spec for configuring, calibrating, and gating both.

Why the Form Factors Diverge

The diagram below contrasts the two collection profiles and shows how the same metric resolves to different ceilings because the underlying hardware and network differ.

Mobile versus desktop budget divergence A mobile profile with 4x CPU slowdown and 1600 kbps 4G and a desktop profile with 1x CPU and 10000 kbps cable each feed a distinct Lighthouse preset and threshold set into one CI gate, which blocks the merge if either form factor breaches its own budget. Mobile profile CPU 4x slowdown 4G · 1600 kbps · 150 ms RTT LCP ≤ 2500 ms · JS ≤ 150 KB breach = blocking error Desktop profile CPU 1x (no slowdown) Cable · 10000 kbps · 40 ms RTT LCP ≤ 2000 ms · JS ≤ 250 KB breach = warning CI gate (matrix) either breach blocks merge
Each form factor runs its own throttling profile and threshold set; the matrix CI gate evaluates both, so neither environment hides a regression in the other.

Prerequisites & Environment

Maintaining two budgets means two collection configs and a CI runner that can execute both deterministically.

  • @lhci/cli ≥ 0.13 with two configs — lighthouserc-mobile.json and lighthouserc-desktop.json, version-controlled side by side.
  • Simulated throttling — use throttlingMethod: simulate on both so timings are reproducible on shared runners rather than dependent on the runner's real bandwidth; for the runner-calibration details see Device & Network Emulation Weighting.
  • Field data per form factor — the P75 mobile and P75 desktop values from CrUX or your RUM, kept separate; never average across devices.
  • A matrix-capable CI — GitHub Actions strategy.matrix to fan the two profiles into parallel jobs.

Configuration Reference

Keep two presets, identical in structure and differing only in the throttling and preset fields. The mobile config applies a 4x CPU slowdown and a constrained 4G network; the desktop config runs unthrottled CPU over cable. The annotated pair below is the authoritative spec.

{
  "ci": {
    "collect": {
      "url": ["https://staging.example.com/"],
      "numberOfRuns": 3,
      "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 }],
        "resource-summary:script:size": ["error", { "maxNumericValue": 150000 }]
      }
    }
  }
}
{
  "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 }],
        "resource-summary:script:size": ["error", { "maxNumericValue": 250000 }]
      }
    }
  }
}

CLS is held identical across both files because visual stability is viewport-agnostic; the script and LCP ceilings diverge because bytes and CPU cost more on the throttled path. For the concrete two-config setup and assertion blocks, see Separate Mobile and Desktop Lighthouse Budgets.

Step-by-Step Implementation

  1. Scaffold the two configs at the repository root.

    touch lighthouserc-mobile.json lighthouserc-desktop.json
    npx lhci healthcheck --fatal

    Expected output: ✅ Healthcheck passed! confirming Chrome and config are reachable.

  2. Run each profile locally against a built preview to confirm the metrics diverge as expected.

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

    Expected output: two assertion summary tables; mobile LCP should be visibly higher than desktop for the same page, confirming the throttling is applied.

  3. Commit both configs once each passes against the current baseline so the divergence is version-controlled and reviewable.

Threshold Calibration

Derive each ceiling from that form factor's field data, never a shared average. Pull the P75 mobile and P75 desktop value for each metric separately, then set the lab assertion 10–15% tighter to absorb the lab-to-field gap. The matrix below shows representative starting points; reconcile them against Core Web Vitals Budget Allocation.

Metric Mobile (4G, 4x CPU, P75) Desktop (Cable, 1x CPU, P75) Rationale for the split
LCP 2500 ms 2000 ms Slower network + CPU push the mobile render later
INP 200 ms 200 ms Interaction budget held constant; both must feel responsive
CLS 0.1 0.1 Visual stability is viewport-agnostic
Script (brotli) 150 KB 250 KB Mobile CPU pays more per parsed byte
TBT 200 ms 150 ms 4x throttling inflates main-thread blocking on mobile

Set newly split thresholds to warn until they hold for two consecutive weeks of baselines, then promote mobile to a hard error. For the mid-range mobile chunking strategy behind the 150 KB script ceiling, see JavaScript Bundle Size Limits.

CI Enforcement Snippet

This matrix job fans the two profiles into parallel runs, retains each report as an artifact, and surfaces a status check per form factor. Branch protection requires both.

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

jobs:
  budget-check:
    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: Run Lighthouse CI (${{ matrix.device }})
        run: npx lhci autorun --config=./lighthouserc-${{ matrix.device }}.json
      - name: Upload reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: lighthouse-reports-${{ matrix.device }}
          path: .lighthouseci/
          retention-days: 30

fail-fast: false ensures a mobile failure does not cancel the desktop job, so a single run reports both verdicts. Scale this across routes as well as form factors with GitHub Actions Performance Matrices.

Troubleshooting & Edge Cases

  • Mobile and desktop scores are suspiciously close → the throttling is not applied; confirm cpuSlowdownMultiplier: 4 and the 4G throttling block are in the mobile config and that preset: mobile is set.
  • Both jobs read the same config → the matrix variable is not interpolated into the --config path; verify lighthouserc-${{ matrix.device }}.json resolves to two distinct files.
  • Desktop noise blocks merges on minor swings → set desktop assertions to warn and keep mobile at error, prioritizing the constrained user path.
  • Field data averaged across devices → split CrUX/RUM by form factor before deriving thresholds; an average hides the mobile tail you most need to protect.
  • One form factor regresses while the other improves → that is exactly what divergence catches; do not net the two — gate each independently.
  • Flaky mobile LCP → raise numberOfRuns to 5 on the mobile config; 4x throttling amplifies single-sample noise.

Frequently Asked Questions

Why not just use the stricter budget for both form factors?

The mobile budget is realistic for a throttled phone but artificially tight for desktop, where it would block legitimate, fast-loading features. The desktop budget is comfortable for fiber but far too loose to protect the mobile user who defines your field scores. Each environment needs a ceiling derived from its own P75 field data — see Core Web Vitals Budget Allocation.

Should mobile and desktop failures be treated the same in CI?

Many teams gate mobile as a blocking error and desktop as a warn, because the constrained mobile path is where regressions hurt real users most. Use fail-fast: false in the matrix so one form factor's failure does not cancel the other's job, and both verdicts appear in a single run.

Which throttling method keeps the two profiles deterministic?

Use throttlingMethod: simulate on both configs. It models CPU and network in software, so results are reproducible on shared CI runners; real-network throttling varies with the runner's actual bandwidth. Calibrate the CPU multiplier against your runner in Device & Network Emulation Weighting.