---
title: "Instrument Core Web Vitals in production and block regressions in CI"
description: "Collect LCP, INP, and CLS from real users, alert on CrUX data, and enforce performance budgets in CI before a slow build ships."
canonical: "https://scalewithsearch.com/articles/core-web-vitals-developer-guide"
date: "2026-03-20"
modified: "2026-09-25"
---
## Site navigation

- [Scale With Search](https://scalewithsearch.com/)
- Real estate
  - Real estate
    - [Real estate](https://scalewithsearch.com/for/real-estate)
- Work
  - Start here
    - [Send your brief](https://scalewithsearch.com/work#send-your-brief)
    - [Prepare your six-question brief](https://scalewithsearch.com/work#prepare-your-six-question-brief)
  - Build
    - [Site build, content library with SEO, signal desk](https://scalewithsearch.com/work)
- For your business
  - Trades and home services
    - [Auto body and collision shops](https://scalewithsearch.com/for/auto-body-and-collision-shops)
    - [Foundation and home repair contractors](https://scalewithsearch.com/for/foundation-and-home-repair)
    - [Garage door and fencing contractors](https://scalewithsearch.com/for/garage-door-and-fencing-contractors)
    - [HVAC contractors](https://scalewithsearch.com/for/hvac-contractors)
    - [Janitorial and commercial cleaning companies](https://scalewithsearch.com/for/janitorial-and-commercial-cleaning)
    - [Locksmiths](https://scalewithsearch.com/for/locksmiths)
    - [Moving companies](https://scalewithsearch.com/for/moving-companies)
    - [Pest control companies](https://scalewithsearch.com/for/pest-control-companies)
    - [Plumbing and electrical contractors](https://scalewithsearch.com/for/plumbing-and-electrical-contractors)
    - [Restoration and water or fire damage companies](https://scalewithsearch.com/for/restoration-and-water-fire-damage)
    - [Roofing companies](https://scalewithsearch.com/for/roofing-companies)
    - [Towing companies](https://scalewithsearch.com/for/towing-companies)
    - [Tree services and landscaping companies](https://scalewithsearch.com/for/tree-services-and-landscaping)
    - [Solar installers](https://scalewithsearch.com/for/solar-installation)
    - [General contractors](https://scalewithsearch.com/for/general-contractors-and-construction)
    - [Paving, concrete, and flooring contractors](https://scalewithsearch.com/for/paving)
  - Practices and professional services
    - [Bookkeeping and tax practices](https://scalewithsearch.com/for/bookkeeping-and-tax-practices)
    - [Dental practices](https://scalewithsearch.com/for/dental-practices)
    - [Family and criminal defense law firms](https://scalewithsearch.com/for/family-and-criminal-defense-law-firms)
    - [Med spas and aesthetics practices](https://scalewithsearch.com/for/med-spas-and-aesthetics)
    - [Personal injury law firms](https://scalewithsearch.com/for/personal-injury-law-firms)
    - [Veterinary clinics](https://scalewithsearch.com/for/veterinary-clinics)
    - [Gyms and fitness studios](https://scalewithsearch.com/for/fitness)
    - [Therapy and outpatient health practices](https://scalewithsearch.com/for/therapy-and-outpatient-health)
    - [Medical billing companies](https://scalewithsearch.com/for/medical-billing)
    - [Insurance agencies](https://scalewithsearch.com/for/insurance-agencies)
    - [Financial advisors](https://scalewithsearch.com/for/financial-advisors)
    - [Property management companies](https://scalewithsearch.com/for/property-management)
    - [Recruiting and staffing agencies](https://scalewithsearch.com/for/recruiting-and-staffing)
    - [Architects and interior designers](https://scalewithsearch.com/for/architects-and-interior-designers)
    - [Logistics and supply chain companies](https://scalewithsearch.com/for/logistics-and-supply-chain)
  - Agencies, MSPs, and manufacturing
    - [IT and managed service providers](https://scalewithsearch.com/for/it-and-managed-service-providers)
    - [Machine shops and precision manufacturers](https://scalewithsearch.com/for/machine-shops-and-precision-manufacturing)
    - [Marketing agencies and freelancers](https://scalewithsearch.com/for/marketing-agencies-and-freelancers)
    - [SEO agencies and consultants](https://scalewithsearch.com/for/seo-agencies-and-consultants)
    - [Small manufacturers and fabricators](https://scalewithsearch.com/for/small-manufacturers-and-fabricators)
  - Restaurants, shops, studios, and nonprofits
    - [Restaurants and hospitality businesses](https://scalewithsearch.com/for/restaurants-and-hospitality)
    - [Retail stores and ecommerce sellers](https://scalewithsearch.com/for/retail-and-ecommerce)
    - [Photographers, event planners, and travel agents](https://scalewithsearch.com/for/photographers)
    - [Churches and nonprofits](https://scalewithsearch.com/for/churches-and-nonprofits)
  - [All industries](https://scalewithsearch.com/for/)
- Learn
  - For your office
    - [Office job guides](https://scalewithsearch.com/guides/)
    - [Browser calculators](https://scalewithsearch.com/tools/)
  - Start here
    - [How it works](https://scalewithsearch.com/how-it-works)
    - [Free Starter Kit](https://scalewithsearch.com/kit/business-memory-starter-kit.zip)
    - [Synthetic specimen](https://scalewithsearch.com/specimen/working-session-specimen.zip)
  - Guides
    - [The Complete Guide to Business Memory for AI Agents](https://scalewithsearch.com/articles/business-memory-for-ai-agents-guide)
    - [The Complete Small-Business Guide to AI Agent Governance](https://scalewithsearch.com/articles/ai-agent-governance-guide-small-business)
    - [The Complete Guide to Leaving Vendor AI Memory](https://scalewithsearch.com/articles/leaving-vendor-ai-memory-guide)
  - Articles by cluster
    - [Business memory](https://scalewithsearch.com/articles/business-memory-for-ai-agents-guide)
    - [Agent governance](https://scalewithsearch.com/articles/ai-agent-governance-guide-small-business)
    - [Migration and ownership](https://scalewithsearch.com/articles/leaving-vendor-ai-memory-guide)
  - For machines
    - [llms.txt](https://scalewithsearch.com/llms.txt)
    - [llms-full.txt](https://scalewithsearch.com/llms-full.txt)
    - [Machine view](https://scalewithsearch.com/?view=machine)
- Company
  - Evidence
    - [Proof](https://scalewithsearch.com/proof)
  - Company
    - [About](https://scalewithsearch.com/about)

# Instrument Core Web Vitals in production and block regressions in CI.

A team fixes its Core Web Vitals, and Search Console turns green. Three releases later it turns red again. Nobody saw it coming, because the only measurement was a monthly look at a report with a 28-day lag.

Core Web Vitals are Google's three user experience metrics. Largest Contentful Paint (LCP) measures loading, and Interaction to Next Paint (INP) measures responsiveness. Cumulative Layout Shift (CLS) measures visual stability. Google's ranking systems use them, so performance work is also SEO work.

This guide covers the measurement side: real user data, alerts, and budgets in the build pipeline. To find the cause of a specific failure, read [Core Web Vitals debugging](/articles/core-web-vitals-debugging). For the fix patterns per metric, read [Core Web Vitals engineering](/articles/core-web-vitals-engineering).

## Decide what the pipeline must answer

Your pipeline answers three questions, each with a different tool:

| Question | Data | Tool |
|---|---|---|
| What do real users experience, per page and per release? | Field data from your own visitors | The `web-vitals` library and your analytics endpoint |
| What does Google see? | Field data from Chrome users, 28-day rolling window | The Chrome UX Report (CrUX) API and Search Console |
| Will this pull request make it worse? | Lab data from a controlled run | Lighthouse CI in your CI system |

Google assesses each metric at the 75th percentile of real page loads. The good thresholds are LCP of 2.5 seconds or less, INP of 200 ms or less, and CLS of 0.1 or less. Lab data diagnoses problems. Field data validates the result. A page can score perfectly in the lab while real users on slow phones and networks fail it.

## Collect field data with the web-vitals library

Google's `web-vitals` library measures the three metrics in the browser the same way Chrome reports them. Install it:

```bash
npm install web-vitals
```

Send each metric to your own endpoint. Include the page path so that you can group results by template:

```javascript
import {onCLS, onINP, onLCP} from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    id: metric.id,
    navigationType: metric.navigationType,
    page: location.pathname
  });
  // sendBeacon survives page unload; fetch with keepalive is the fallback
  if (!navigator.sendBeacon('/analytics', body)) {
    fetch('/analytics', {method: 'POST', body, keepalive: true});
  }
}

onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
```

The library reports INP and CLS when the page is hidden, because both accumulate over the page's life. The `rating` field already classifies each value as good, needs improvement, or poor.

## Receive the beacons on your server

A beacon with a string body arrives as `text/plain`, so parse it as text:

```javascript
// Express endpoint
app.post('/analytics', express.text({type: '*/*'}), (req, res) => {
  const {name, value, rating, id, page} = JSON.parse(req.body);
  analyticsDB.insert({
    metric: name, value, rating, sessionId: id, page,
    userAgent: req.headers['user-agent'],
    timestamp: new Date()
  });
  res.sendStatus(204);
});
```

Store raw values, not averages. Core Web Vitals are judged at the 75th percentile, and an average hides the slow tail. In PostgreSQL, the p75 LCP for a template is one query:

```sql
SELECT percentile_cont(0.75) WITHIN GROUP (ORDER BY value)
FROM vitals WHERE metric = 'LCP' AND page LIKE '/products/%';
```

## Add attribution when you need a cause

The attribution build adds the diagnostic detail that tells you why a value is slow. It costs more bytes, so many teams ship it to a sample of sessions or turn it on while they debug:

```javascript
import {onLCP} from 'web-vitals/attribution';

onLCP(({value, attribution}) => {
  sendToAnalytics({name: 'LCP', value,
    element: attribution.element,
    ttfb: attribution.timeToFirstByte,
    loadDelay: attribution.resourceLoadDelay,
    loadDuration: attribution.resourceLoadDuration,
    renderDelay: attribution.elementRenderDelay});
});
```

These field names apply to `web-vitals` version 4 and later, released in 2024. Version 4 renamed several attribution fields, so check the version before you copy older samples.

## Treat single-page applications with care

In a single-page application, INP and CLS keep accumulating across client-side route changes. Field data attributes them to the URL of the first full page load. Record the current route with each beacon, as the sample above does. You can then see which view produced a slow interaction.

Single-page applications also struggle with LCP, because the content does not exist until JavaScript runs. Render the first view on the server or at build time with Next.js, Nuxt, or SvelteKit. For content pages, static generation with periodic regeneration is the usual fit. These examples use the Next.js Pages Router; in the App Router, server components fetch data on the server by default.

```javascript
// pages/blog/[slug].js: static page, regenerated at most once an hour
export async function getStaticProps({params}) {
  return {props: {post: await fetchPost(params.slug)}, revalidate: 3600};
}

export async function getStaticPaths() {
  const posts = await fetchAllPosts();
  return {paths: posts.map(p => ({params: {slug: p.slug}})), fallback: 'blocking'};
}
```

For pages that must be fresh on every request, `getServerSideProps` fetches the data on the server so that the HTML arrives complete. For framework-level trade-offs, read [JavaScript SEO frameworks](/articles/javascript-seo-frameworks).

## Watch what Google sees with the CrUX API

Your own data shows every visit. CrUX shows the Chrome data that Google uses. Check it on a schedule and alert when the 75th percentile crosses a threshold:

```javascript
async function checkCoreWebVitals(url) {
  const response = await fetch(
    `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${API_KEY}`,
    {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({
        url,
        formFactor: 'PHONE',
        metrics: ['largest_contentful_paint', 'interaction_to_next_paint', 'cumulative_layout_shift']
      })
    }
  );
  const {record} = await response.json();
  if (record.metrics.largest_contentful_paint.percentiles.p75 > 2500) {
    sendAlert(`LCP p75 over 2,500 ms on ${url}`);
  }
}
```

Query the phone form factor first. Google uses mobile-first indexing, and phones are usually the harder case. CrUX returns data only for URLs with enough traffic; for smaller pages, query by origin instead.

## Enforce budgets in CI with Lighthouse CI

Budgets stop the slow accumulation of small regressions. Run Lighthouse CI on every pull request against a staging build. Save the GitHub Actions job as `.github/workflows/lighthouse.yml`:

```yaml
name: Lighthouse CI
on: [pull_request]
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v9   # check for a newer major version
        with:
          urls: |
            https://staging.example.com/
            https://staging.example.com/product
          configPath: ./lighthouserc.json
          uploadArtifacts: true
```

Put the budgets in `lighthouserc.json` as assertions. Sizes are in bytes:

```json
{
  "ci": {
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", {"maxNumericValue": 2500}],
        "cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}],
        "total-blocking-time": ["warn", {"maxNumericValue": 200}],
        "resource-summary:script:size": ["error", {"maxNumericValue": 300000}],
        "resource-summary:image:size": ["warn", {"maxNumericValue": 500000}],
        "resource-summary:total:size": ["error", {"maxNumericValue": 1000000}]
      }
    }
  }
}
```

Lighthouse loads the page without user input, so it cannot measure INP. Total Blocking Time is the lab proxy. Keep INP in your field monitoring. The workflow in more detail is in [Lighthouse in GitHub Actions](/articles/lighthouse-github-actions-seo), and the wider test setup in [SEO testing in a CI/CD pipeline](/articles/seo-testing-ci-cd-pipeline).

To see what fills a JavaScript budget, add `webpack-bundle-analyzer` with `analyzerMode: 'static'`. It writes an HTML report of every module in each bundle.

## Build the common fixes into the platform

Some fixes belong in shared infrastructure, so that each page gets them without extra work.

Cache static assets for a year with `immutable`, and keep HTML revalidating. In Express, `express.static('public', {maxAge: '1y', immutable: true})` does this, with a `setHeaders` hook that sends `Cache-Control: no-cache` for `.html` files. Cache slow dynamic data in Redis, for example with `client.setEx(key, 300, value)` for five minutes. A Cloudflare Worker can apply `Cache-Control: public, max-age=31536000, immutable` to `/static/` paths at the edge.

Generate modern image formats at build time. In webpack, `image-minimizer-webpack-plugin` with the `sharp` encoder can emit WebP at quality 80 and AVIF at quality 70. Serve them in a `picture` element with a JPEG fallback and explicit `width` and `height`.

Load third-party scripts late. For analytics that does not need the first second, a small loader creates the script element late. It waits for the `load` event plus a short delay, for example 2 seconds. Partytown moves a third-party script into a web worker; you mark the script element with `type="text/partytown"`.

Keep the main thread free. Split heavy components with `lazy()` and dynamic `import()`. Use `splitChunks` in webpack to separate vendor code. Move computation into Web Workers; Comlink lets you call a worker like an async function. Register `touchstart` and `wheel` listeners with `{passive: true}`. Batch scroll-driven visual updates in `requestAnimationFrame`.

Keep layouts stable. Give video iframes a container with `aspect-ratio: 16 / 9`. Reserve ad slots with `min-height`. Show skeleton screens where content loads late. Use `font-display: optional` and preload the main font. Subset Google Fonts with the `text=` parameter when a font only renders a few words. Animate `transform` and `opacity` only, and isolate widgets with `contain: layout style paint`.

## Work one example from beacon to budget

The inputs in this example are hypothetical. A store's product page template gets a week of field beacons. The p75 LCP on phones is 3.4 seconds, in the poor range. The attribution beacons show that resource load delay is the largest part. The hero image is a CSS background, so the browser finds it only after it parses the stylesheet.

The team changes the hero to an `img` element with `fetchpriority="high"`, explicit dimensions, and AVIF and WebP sources. It adds a preload for the image in the head. It adds the product template URL to the Lighthouse CI job, with the LCP assertion at 2,500 ms. A future change that delays the image then fails the pull request.

Then the team measures. It records the p75 LCP from its own beacons for the week before and the week after the release. It checks CrUX for the template URL over the following 28 days, and Search Console after that. The example stops at the measurement: record your own before and after values, not an expected gain.

## Keep a cadence

Monitor continuously through your beacons, CrUX alerts, and Search Console. After a major deployment, confirm within 48 hours that the field values have not regressed. Run a focused optimization sprint each quarter, or when Search Console flags a new issue.

Most optimizations improve experience without removing features, but some trade-offs exist. Aggressive code splitting adds requests. Lazy loading delays a feature until the user needs it. A removed third-party script takes its function with it. When a trade-off is real, favor what the user sees over background analytics and non-essential features.

----

```text
                  .|########||.                                       .|########||.                                       .|########||.
               |##||.      .||##|.                                 |##||.      .||##|.                                 |##||.      .||##|.
             |#|.              .|#|.                             |#|.              .|#|.                             |#|.              .|#|.
           |#|                    |#|                          |#|                    |#|                          |#|                    |#|
         .#|                        |#.                      .#|                        |#.                      .#|                        |#.
        .#.                          .#|                    .#.                          .#|                    .#.                          .#|
       |#.                            .#|                  |#.                            .#|                  |#.                            .#|
      |#             ......             #|                |#             ......             #|                |#             ......             #|
     .#           ||#########|           #|              .#           ||#########|           #|              .#           ||#########|           #|
    .#.         |######||######|.        .#.            .#.         |######||######|.        .#.            .#.         |######||######|.        .#.
    #.        .##|###|##|#|######|        .#            #.        .##|###|##|#|######|        .#            #.        .##|###|##|#|######|        .#
   ||        |##|#||||||||||||#||#|        ||          ||        |##|#||||||||||||#||#|        ||          ||        |##|#||||||||||||#||#|        ||
   #        |#||||||||||||||||||||#|        #.         #        |#||||||||||||||||||||#|        #.         #        |#||||||||||||||||||||#|        #.
  ||       |#||||||||||||||||||||||#|       ||        ||       |#||||||||||||||||||||||#|       ||        ||       |#||||||||||||||||||||||#|       ||
  #       .#||||||||||||||||||||||||#|       #        #       .#||||||||||||||||||||||||#|       #        #       .#||||||||||||||||||||||||#|       #
 ||   ....|||#||||||##|#|||#|#||##||||....|. ||      ||   ....|||#||||||##|#|||#|#||##||||....|. ||      ||   ....|||#||||||##|#|||#|#||##||||....|. ||
 #.  .  ....|#  ....#|||   ||| .#|||  ....#. .#      #.  .  ....|#  ....#|||   ||| .#|||  ....#. .#      #.  .  ....|#  ....#|||   ||| .#|||  ....#. .#
 #   .  ||||#| .#####||  . .#| .#|||  ||||#   #.     #   .  ||||#| .#####||  . .#| .#|||  ||||#   #.     #   .  ||||#| .#####||  . .#| .#|||  ||||#   #.
.|   |||||  #. |#|||||. ||  #. |#||. .|||||   ||    .|   |||||  #. |#|||||. ||  #. |#||. .|||||   ||    .|   |||||  #. |#|||||. ||  #. |#||. .|||||   ||
||   |....  #. ....|#.      |. ...|. ....||   ||    ||   |....  #. ....|#.      |. ...|. ....||   ||    ||   |....  #. ....|#.      |. ...|. ....||   ||
#.  .||||||##||||||##||####|||||||#||||||#|   .#    #.  .||||||##||||||##||####|||||||#||||||#|   .#    #.  .||||||##||||||##||####|||||||#||||||#|   .#
#    .||####|###########################|.     #    #    .||####|###########################|.     #    #    .||####|###########################|.     #
#      .#||||||.#.|| # |. ..# #| #|||||#|      #    #      .#||||||.#.|| # |. ..# #| #|||||#|      #    #      .#||||||.#.|| # |. ..# #| #|||||#|      #
#      .#|||||| ..  |# ## |#| ...#|#|||#|      #    #      .#|||||| ..  |# ## |#| ...#|#|||#|      #    #      .#|||||| ..  |# ## |#| ...#|#|||#|      #
#      .#|||#|# .# .#| #| ##|.#..#|||||#|      #    #      .#|||#|# .# .#| #| ##|.#..#|||||#|      #    #      .#|||#|# .# .#| #| ##|.#..#|||||#|      #
#      .#||||||############|######|||||#|      #    #      .#||||||############|######|||||#|      #    #      .#||||||############|######|||||#|      #
#   |...||#||||||#||||#|#||||||#|||||#||| |.|  #    #   |...||#||||||#||||#|#||||||#|||||#||| |.|  #    #   |...||#||||||#||||#|#||||||#|||||#||| |.|  #
#. .. ||||# .|||##|.  |#| .|| || .|||#. #|  # .#    #. .. ||||# .|||##|.  |#| .|| || .|||#. #|  # .#    #. .. ||||# .|||##|.  |#| .|| || .|||#. #|  # .#
|| |  ...||  ...##| |  #| .|. |. #####  .. .| ||    || |  ...||  ...##| |  #| .|. |. #####  .. .| ||    || |  ...||  ...##| |  #| .|. |. #####  .. .| ||
|| ||||| || ||||#|  .  |. |. |#. ||||| |#| || ||    || ||||| || ||||#|  .  |. |. |#. ||||| |#| || ||    || ||||| || ||||#|  .  |. |. |#. ||||| |#| || ||
.# |.....#|....|#.||||.|||##.|#|....||.#||.#. #.    .# |.....#|....|#.||||.|||##.|#|....||.#||.#. #.    .# |.....#|....|#.||||.|||##.|#|....||.#||.#. #.
 #.|||||||#############################| ||| .#      #.|||||||#############################| ||| .#      #.|||||||#############################| ||| .#
 ||       ##||#||#||||||#||#||#||#||##.      ||      ||       ##||#||#||||||#||#||#||#||##.      ||      ||       ##||#||#||||||#||#||#||#||##.      ||
  #       .#|||||||||#||#|||||||||||#|       #        #       .#|||||||||#||#|||||||||||#|       #        #       .#|||||||||#||#|||||||||||#|       #
  ||       |#||||||||||||||||||||||#|       ||        ||       |#||||||||||||||||||||||#|       ||        ||       |#||||||||||||||||||||||#|       ||
  .#        |#||||||||||||||||||||#|        #.        .#        |#||||||||||||||||||||#|        #.        .#        |#||||||||||||||||||||#|        #.
   ||        |#||||||||||||||||||#|        ||          ||        |#||||||||||||||||||#|        ||          ||        |#||||||||||||||||||#|        ||
    #.        .######|#|#########|        .#            #.        .######|#|#########|        .#            #.        .######|#|#########|        .#
    .#.         |#####||||#####|         .#.            .#.         |#####||||#####|         .#.            .#.         |#####||||#####|         .#.
     |#           ||########||           #.              |#           ||########||           #.              |#           ||########||           #.
      |#             ......             #|                |#             ......             #|                |#             ......             #|
       |#.                            .#|                  |#.                            .#|                  |#.                            .#|
        |#.                          .#.                    |#.                          .#.                    |#.                          .#.
         .#|                        |#.                      .#|                        |#.                      .#|                        |#.
           |#|                    |#|                          |#|                    |#|                          |#|                    |#|
            .|#|.              .|#|.                            .|#|.              .|#|.                            .|#|.              .|#|.
               |##||.      .||##|                                  |##||.      .||##|                                  |##||.      .||##|
                 .||########||.                                      .||########||.                                      .||########||.

Scale With Search  2026  [scalewithsearch.com](https://scalewithsearch.com)
```
