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.
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 inpackage-lock.jsonso assertions are reproducible across machines.- A budget manifest location — a version-controlled
budget-manifest.jsonat the repository root, the single source of truth every team reads from. - CPU/network throttling alignment — CI must run
throttlingMethod: simulatewith 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
-
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.p75Expected output: a single millisecond value (for example
2310) you compare against the 2500 ms ceiling. -
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
<800mswith 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.
-
Partition the INP execution budget. Cap main-thread blocking at
<50msper event handler, yield to the main thread withscheduler.yield()(or offload to a Web Worker), and flag interactions over 200 ms with aPerformanceObserver.// 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
reportSlowInteractioncall carryinginteractionIdandduration. For data-dense interfaces, the dashboard-specific method is in Calculating INP Thresholds for Interactive Dashboards. -
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
inpassertion key in Lighthouse → expected; INP needs real interactions. Gatetotal-blocking-timesynthetically 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.