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 roughly 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. If you also run a client-rendered app where route changes never touch the network stack the same way a cold load does, pair this with Single-Page App Performance Budgets, which handles the soft-navigation case that neither a mobile nor a desktop cold-load budget captures on its own.

Why the Form Factors Diverge

The gap is not a matter of taste or brand preference — it is physics. A mid-range mobile on Fast 3G or throttled 4G renders the same HTML, parses the same JavaScript, and decodes the same images as a desktop on cable, but it does so with a CPU that Lighthouse models at a 4x slowdown and a network with roughly four times the round-trip latency. A resource that costs 40 ms of main-thread work on desktop costs closer to 160 ms on that phone, and a payload that streams in 200 ms over cable takes well over a second over constrained mobile. Applying one LCP ceiling of, say, 2500 ms at P75 to both means desktop coasts under it with a full second of unused headroom while mobile fights for every millisecond — the exact asymmetry that lets a real regression hide.

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.

The Physics of the Gap

To calibrate two budgets you have to understand which costs scale with the device and which do not. Network transfer time scales with throughput and latency: a 150 KB brotli script that arrives in about 130 ms over a 10000 kbps cable link takes well over 800 ms over a 1600 kbps mobile link once you add the 150 ms round trip and TCP ramp-up. CPU-bound work — script parse, compile, and execution, plus style and layout — scales with the CPU multiplier: the same bundle that Lighthouse charges roughly 150 ms to parse and compile on an unthrottled desktop is charged closer to 600 ms under the 4x mobile slowdown. That is why the two budgets diverge in opposite directions on different metrics. Byte ceilings tighten for mobile because every kilobyte is dearer on the throttled main thread; timing ceilings loosen for mobile because the same work legitimately finishes later.

The chart below quantifies the CPU half of that story: the identical bundle, measured on the same page, charged very different main-thread time by device class.

Main-thread cost of the same bundle by device The same 200 KB script costs about four times more main-thread parse and compile time on throttled mobile than on unthrottled desktop. Main-thread time to parse and compile 200 KB JS 0 200 400 600 Main-thread time (ms) ~600 ms Mobile · 4x CPU ~150 ms Desktop · 1x CPU
Identical code, four times the main-thread cost on throttled mobile — the reason byte budgets must tighten even as timing budgets loosen.

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 own RUM, kept separate; never average across devices. If you are standing up that field pipeline, Custom Performance Beacons & RUM covers how to collect the per-device percentiles these budgets depend on.
  • 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. Notice one subtlety worth calling out: the throughputKbps and rttMs values are not arbitrary — they should mirror the connection class your field data says your P75 mobile session actually runs on, which for most consumer audiences is closer to throttled 4G than to true 3G. If your CrUX report shows a slower effective connection type at P75, lower throughputKbps accordingly rather than keeping a runner-friendly default. 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. Diff the two reports to sanity-check that the divergence is real and not an artifact of a warm cache or a flaky single run.

    npx lhci open

    Expected output: the local report viewer opens; compare the mobile and desktop LCP filmstrips. If mobile LCP is not at least 30–50% higher than desktop for the same page at P75, the throttling is likely not being applied and you should stop before committing thresholds.

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

Choosing How Many Profiles to Maintain

Two profiles is the default, not a law. The right number is whatever your field data says carries meaningful traffic, and the decision is driven entirely by session share at P75, never by which devices the team happens to own. Most sites need exactly two, but a media site with heavy tablet readership or a B2B tool that is 90% desktop should adjust. Use the tree below to decide, then set each new profile to warn before promoting it.

Choosing how many device profiles to gate A decision tree that starts from per-form-factor field data and branches on mobile session share to decide whether the mobile budget is a blocking error or a warning. Pull P75 field data split by form factor (CrUX / RUM) Mobile ≥ 20% of sessions? Yes No Separate mobile budget gate mobile as blocking error desktop as warn or error Desktop-lean budget keep mobile as warn promote when traffic grows
Session share at P75 — not device inventory — decides whether the mobile budget blocks a merge or only warns.

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 for a mid-range mobile on throttled 4G and a desktop on cable; 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
Image weight 300 KB 600 KB Constrained mobile bandwidth caps hero + gallery bytes harder

Set newly split thresholds to warn until they hold for two consecutive weeks of baselines, then promote mobile to a hard error. Whether you calibrate each ceiling to the P75 or the stricter P90 of your field data is a deliberate trade between protecting the median user and protecting the tail — Choosing Between P75 and P90 Budget Targets walks through when the tighter target is worth the extra false positives. For the mid-range mobile chunking strategy behind the 150 KB script ceiling, see JavaScript Bundle Size Limits; for the image weight row, see Image & Media Weight Budgets.

One calibration trap to avoid: do not let the desktop timing ceiling be looser than what desktop field data actually supports just because it is a fast environment. A desktop LCP at P75 that CrUX reports around 1400 ms should be gated near 1600–1800 ms, not left at a lazy 2500 ms inherited from the mobile file. A slack desktop budget quietly re-creates the single-budget problem in reverse: it stops catching desktop-only regressions such as a render-blocking third-party script that a phone would never reach because the page timed out first.

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. Because each matrix leg publishes its own status check (budget-check (mobile) and budget-check (desktop)), you can require both in branch protection and still see at a glance which form factor broke. The flow below shows how the two legs recombine into a single merge decision.

Matrix gate: both form factors must pass A pull request spawns parallel mobile and desktop Lighthouse jobs whose status checks combine so that either failing verdict blocks the merge. PR opened build + fan out Mobile job 4G · 4x CPU · error Desktop job cable · 1x CPU · warn Merge gate both green → merge either red → block
Two parallel legs, two independent status checks, one merge decision — the desktop leg can never absorb a mobile regression, and vice versa.

Scale this across routes as well as form factors with GitHub Actions Performance Matrices: a two-dimensional matrix of device × route gives every page its own per-form-factor verdict without duplicating workflow YAML.

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. On a correctly throttled run the mobile LCP for a typical content page should sit 30–60% above desktop at P75.
  • 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 at P75.
  • 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, so the extra runs tighten the median the assertion reads.
  • A shared component regresses only on desktop → this happens when a feature is behind a viewport or capability check and only ships to wide screens; the desktop leg is the only place it will surface, which is a second reason not to demote desktop to warn for LCP on desktop-heavy products.

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. On desktop-dominant products, keep desktop at error too so desktop-only regressions still block.

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.

How many device profiles should we actually maintain?

Base it on session share at P75, not on which devices the team owns. Most sites need exactly mobile and desktop, each gated separately. Add a third tablet profile only when tablet crosses roughly 10% of sessions, and lean the whole budget toward desktop when a B2B tool is 90% desktop. Start any new profile as a warn and promote it to error once its baseline holds for two weeks.

Do the byte budgets and timing budgets diverge in the same direction?

No — they move oppositely. Byte ceilings tighten for mobile because a throttled 4x CPU pays more per parsed kilobyte, so the mobile script budget is lower than desktop. Timing ceilings loosen for mobile because the same work legitimately finishes later on a slower CPU and 4G network, so mobile LCP is allowed to be higher than desktop. CLS stays identical across both because visual stability is viewport-agnostic.