Core Web Vitals Budget Allocation

Allocating Core Web Vitals budgets means shifting from reactive optimization to proactive resource partitioning: assigning each of Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS) a strict byte-and-time ceiling per route before code reaches production. This guide is part of the Defining Web Performance Budgets reference, and it details how to derive a ceiling from field data, decompose LCP into its sub-parts, partition the INP execution budget across the main thread, and gate the whole thing in CI.

The central idea is that a metric like LCP is never monolithic. It is the sum of network latency, resource load, and render delay, and you cannot manage what you have not decomposed. Allocation starts by breaking each metric into addressable sub-parts, assigning a millisecond or byte ceiling to each, and then enforcing those ceilings independently.

LCP Sub-Part Allocation

The 2500 ms LCP ceiling is an envelope, not a single timer. It splits into Time to First Byte, the resource-load window for the LCP element (image fetch and decode, or font swap), and the render delay before the element paints. Allocate a ms ceiling to each sub-part so a regression in one is isolated and attributable. The diagram below shows a representative split of a 2500 ms P75 budget on high-end mobile over 4G.

LCP sub-part allocation timeline A 2500 ms LCP budget partitioned into a TTFB segment of 800 ms, a resource-load segment of 1100 ms, and a render-delay segment of 600 ms, each with its own ceiling. time to LCP paint (ms) TTFB ≤ 800 ms resource load ≤ 1100 ms (fetch + decode) render delay ≤ 600 ms 0 800 1900 2500
Partition the LCP envelope so each sub-part has an independent ceiling; a render-delay regression is then attributable without re-measuring the whole metric.

Prerequisites & Environment

Budget allocation depends on field data and a deterministic measurement harness. Have the following in place before deriving ceilings:

  • Field data source — the CrUX API or a RUM provider that exposes route-level P75 for LCP, INP, and CLS over a rolling 28-day window, segmented by device class.
  • @lhci/cli ≥ 0.13 and Chrome ≥ 120 — for the synthetic gate, pinned in package-lock.json so assertions are reproducible across machines.
  • A budget manifest location — a version-controlled budget-manifest.json at the repository root, the single source of truth every team reads from.
  • CPU/network throttling alignment — CI must run throttlingMethod: simulate with a 4x CPU slowdown to match the mid-range mobile baseline, the most common source of lab-to-field divergence.

Configuration Reference

Derive the manifest by querying field P75 per route, then subtracting the baseline TTFB reserve from the total LCP target to expose the execution headroom available for resource load and render delay. The remaining headroom is partitioned across rendering, INP responsiveness, and layout-stability margin. The annotated manifest below is the authoritative per-route contract.

{
  "route": "/",
  "device_class": "high_end_mobile",
  "lcp_target_ms": 2500,
  "ttfb_reserve_ms": 800,
  "lcp_resource_load_ms": 1100,
  "lcp_render_delay_ms": 600,
  "inp_target_ms": 200,
  "cls_target": 0.1
}

ttfb_reserve_ms is subtracted first because it is the floor you cannot optimize in the browser; what remains is the budget the front end actually controls. lcp_resource_load_ms bounds the LCP element's fetch and decode, and lcp_render_delay_ms bounds the gap between resource availability and paint. Allocate INP at roughly 40% framework reconciliation and 60% data processing so heavy work never starves an interaction.

Step-by-Step Implementation

  1. Derive the baseline. Query field P75 per route and write the per-sub-part ceilings into the manifest.

    curl -s "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$CRUX_KEY" \
      -H "Content-Type: application/json" \
      -d '{"url":"https://www.example.com/","formFactor":"PHONE"}' \
      | npx json -a record.metrics.largest_contentful_paint.percentiles.p75

    Expected output: a single millisecond value (for example 2310) you compare against the 2500 ms ceiling.

  2. Enforce the LCP critical path. Identify the LCP candidate from a Lighthouse DOM snapshot, then elevate it above the default network queue and cap its decode budget at <800ms with a font swap timeout <100ms.

    <link rel="preload" as="image" href="/img/hero.avif" fetchpriority="high">
    <img src="/img/hero.avif" fetchpriority="high" width="1200" height="600" alt="">

    When the route is image-heavy catalog or storefront traffic, apply the per-page-type method in How to Set Realistic LCP Budgets for E-commerce.

  3. Partition the INP execution budget. Cap main-thread blocking at <50ms per event handler, yield to the main thread with scheduler.yield() (or offload to a Web Worker), and flag interactions over 200 ms with a PerformanceObserver.

    // inp-observer.js
    new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.duration > 200) reportSlowInteraction(entry);
      });
    }).observe({ type: 'event', buffered: true });

    Expected console output during a slow interaction: a single reportSlowInteraction call carrying interactionId and duration. For data-dense interfaces, the dashboard-specific method is in Calculating INP Thresholds for Interactive Dashboards.

  4. Commit the manifest so every subsequent budget change is a reviewable diff.

Threshold Calibration

Do not copy these ceilings untouched; derive each from your own field P75 and set the lab assertion 10–15% tighter to absorb the lab-to-field gap. The matrix below shows representative starting points across the device classes you gate. Hold a new threshold at warn for two consecutive weeks of green baselines before promoting it to error, so the gate earns trust before it blocks merges; the percentile method is detailed in Percentile-Based Threshold Tuning.

Device class Connection LCP P75 (ms) INP P75 (ms) CLS P75
High-end mobile 4G / LTE 2200 180 0.08
Mid-range mobile Fast 3G 3500 300 0.12
Desktop Cable / Fiber 1500 120 0.05

CI Enforcement Snippet

Lighthouse CI gates LCP and CLS directly. INP requires real interactions, so gate Total Blocking Time as the synthetic proxy and validate true INP from RUM. This lighthouserc.json is copy-paste ready.

{
  "ci": {
    "collect": {
      "numberOfRuns": 3,
      "settings": {
        "throttlingMethod": "simulate",
        "throttling": { "cpuSlowdownMultiplier": 4 }
      }
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["error", { "maxNumericValue": 200 }],
        "resource-summary:script:size": ["error", { "maxNumericValue": 150000 }]
      }
    }
  }
}

Run this on pull_request, route warnings to Slack, and require the check in branch protection. Initial and per-route script ceilings that feed these assertions are specified under JavaScript Bundle Size Limits.

Troubleshooting & Edge Cases

  • LCP passes in CI but fails in field → the lab decode budget assumes a faster CPU; widen the mid-range mobile throttle to 6x and re-derive the resource-load ceiling.
  • No inp assertion key in Lighthouse → expected; INP needs real interactions. Gate total-blocking-time synthetically and validate INP from RUM.
  • CLS regresses only on cached navigations → a service worker alters resource timing; validate offline fallback rendering against the CLS ceiling separately from first load.
  • Manifest drift across teams → enforce schema validation on PR so a route cannot ship without explicit per-sub-part ceilings.
  • Render delay dominates a passing resource-load budget → audit render-blocking CSS and long tasks in the critical window; the resource arrived on time but the main thread was busy.
  • Third-party script inflates LCP and INP together → defer it behind consent or requestIdleCallback; cross-check the loading method against Third-Party Script Constraints.

Frequently Asked Questions

Why split LCP into sub-parts instead of gating one number?

A single 2500 ms ceiling tells you a regression happened but not where. Partitioning into TTFB, resource load, and render delay makes each regression attributable: a slow render delay points at render-blocking CSS or long tasks, while a slow resource-load points at the hero image or font. You manage what you decompose.

Can Lighthouse CI gate INP directly?

No. INP is measured from real user interactions across a session, which a synthetic run does not reproduce. Gate total-blocking-time as the lab proxy in CI and validate true INP from RUM at P75. For dashboards with heavy event handlers, see Calculating INP Thresholds for Interactive Dashboards.

How do I split the INP budget across framework work and business logic?

Allocate roughly 40 percent to framework reconciliation and 60 percent to data processing and DOM rendering, then keep any single event handler under 50 ms of main-thread blocking by yielding with scheduler.yield() or offloading to a Web Worker.