Core web vitals optimization skill

Core Web Vitals are Google's standardized performance metrics that measure real user experience.

by wondelai·MIT license·★ 2,235 Stars on the repo·GitHub ↗

Use now

Files of Core web vitals optimization

wondelai/main1 file
core-web-vitals.md
Show the full text447 lines

Core Web Vitals Optimization

Core Web Vitals are Google's standardized performance metrics that measure real user experience. They directly impact search ranking and correlate strongly with user engagement, conversion rates, and bounce rates.

Table of Contents

  1. The Three Core Web Vitals
  2. LCP: Largest Contentful Paint
  3. INP: Interaction to Next Paint
  4. CLS: Cumulative Layout Shift
  5. Measuring Core Web Vitals
  6. Performance Budgets
  7. Debugging Workflow
  8. Quick Reference: Optimization Impact

The Three Core Web Vitals

Metric Measures Good Needs Improvement Poor
LCP (Largest Contentful Paint) Loading performance < 2.5s 2.5s - 4.0s > 4.0s
INP (Interaction to Next Paint) Interactivity < 200ms 200ms - 500ms > 500ms
CLS (Cumulative Layout Shift) Visual stability < 0.1 0.1 - 0.25 > 0.25
Supporting metrics
Metric Measures Target
TTFB (Time to First Byte) Server responsiveness < 800ms
FCP (First Contentful Paint) Initial render speed < 1.8s
TBT (Total Blocking Time) Main thread availability (lab proxy for INP) < 200ms

LCP: Largest Contentful Paint

LCP measures when the largest visible content element finishes rendering. This is what users perceive as "the page has loaded."

What counts as the LCP element
  • <img> elements (including those inside <picture>)
  • <video> poster images
  • Elements with background-image via CSS
  • Block-level text elements (<h1>, <p>, etc.)

The LCP element changes as the page loads. The final LCP element is the one that is largest when rendering stabilizes.

Common LCP problems

Slow server response (high TTFB): The browser cannot render anything until it receives the first byte of HTML.

Fixes:

  • Add a CDN to reduce network latency
  • Implement server-side caching (Redis, Varnish)
  • Use streaming server-side rendering to send the HTML <head> immediately
  • Optimize database queries and backend processing
  • Use 103 Early Hints to let the browser start fetching critical resources before the full response

Late-discovered LCP resource: The browser discovers the LCP image late in the rendering process (e.g., a CSS background image or a <img> tag deep in the HTML).

Fixes:

<!-- Preload the LCP image -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">

<!-- Or use fetchpriority directly -->
<img src="/hero.webp" fetchpriority="high" alt="Hero" width="1200" height="600">

Render-blocking resources: CSS and synchronous JavaScript delay rendering.

Fixes:

  • Inline critical CSS in <head>
  • Defer non-critical CSS loading
  • Use defer or async on scripts
  • Remove unused CSS (audit with Coverage panel)

Slow resource load time: The LCP image itself is too large or served from a slow origin.

Fixes:

  • Compress images (AVIF, WebP)
  • Serve responsive images with srcset
  • Use a CDN for image delivery
  • Set appropriate cache headers

Client-side rendering: SPAs that render content in JavaScript after the initial HTML load have inherently slow LCP.

Fixes:

  • Use server-side rendering (SSR) or static site generation (SSG)
  • Stream HTML with Transfer-Encoding: chunked
  • Pre-render the above-fold content on the server
LCP optimization checklist
  • TTFB under 800ms
  • LCP resource discoverable in HTML source (not CSS or JS)
  • LCP resource preloaded with fetchpriority="high"
  • No render-blocking resources before LCP
  • LCP image optimized (format, compression, responsive)
  • LCP image served from CDN
  • Critical CSS inlined; non-critical CSS deferred

INP: Interaction to Next Paint

INP measures the delay between a user interaction (click, tap, keypress) and the next visual update. It replaced FID (First Input Delay) as a Core Web Vital in March 2024.

Why INP matters more than FID: FID only measured the delay of the first interaction. INP measures all interactions throughout the page lifecycle and reports the single worst one — except on pages with many interactions, where it drops roughly one outlier per 50 interactions (approximating the 98th percentile for high-interaction pages). A page can have a good FID but terrible INP if JavaScript blocks the main thread during later interactions.

What causes poor INP

Long tasks on the main thread: Any JavaScript task that runs for more than 50ms blocks the browser from processing user input.

Excessive event handler work: Click handlers that perform heavy computation, DOM manipulation, or synchronous operations delay the visual response.

Layout thrashing: Reading layout properties (like offsetHeight) and then writing to the DOM in a loop forces the browser to recalculate layout repeatedly.

INP optimization strategies

Break up long tasks:

// BAD: One long task blocking the main thread
function processAllItems(items) {
  items.forEach(item => heavyOperation(item));
}

// GOOD: Yield to the main thread between chunks
async function processAllItems(items) {
  for (const item of items) {
    heavyOperation(item);
    // Yield to let the browser handle pending interactions
    await scheduler.yield();
  }
}

If scheduler.yield() is not available, use a polyfill pattern:

function yieldToMain() {
  return new Promise(resolve => setTimeout(resolve, 0));
}

async function processAllItems(items) {
  for (let i = 0; i < items.length; i++) {
    heavyOperation(items[i]);
    if (i % 10 === 0) await yieldToMain();
  }
}

Defer non-urgent work:

// Use requestIdleCallback for work that does not need to happen immediately
requestIdleCallback(() => {
  analytics.track('page_view');
  prefetchNextPage();
});

Minimize event handler work:

// BAD: Heavy computation in click handler
button.addEventListener('click', () => {
  const result = expensiveCalculation();  // 200ms
  updateDOM(result);                       // 50ms
});

// GOOD: Show immediate feedback, defer heavy work
button.addEventListener('click', () => {
  showLoadingState();                      // 5ms - immediate feedback
  requestAnimationFrame(() => {
    const result = expensiveCalculation();
    updateDOM(result);
    hideLoadingState();
  });
});

Avoid layout thrashing:

// BAD: Read-write-read-write forces repeated layout
elements.forEach(el => {
  const height = el.offsetHeight;        // Read (forces layout)
  el.style.height = height * 2 + 'px';  // Write (invalidates layout)
});

// GOOD: Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight);  // All reads
elements.forEach((el, i) => {
  el.style.height = heights[i] * 2 + 'px';            // All writes
});

Reduce JavaScript payload:

  • Code-split: load only what the current page needs
  • Tree-shake: remove unused exports
  • Lazy-load non-critical modules
  • Defer third-party scripts (analytics, widgets)
INP optimization checklist
  • No JavaScript tasks longer than 50ms on the main thread
  • Event handlers provide immediate visual feedback
  • Non-urgent work deferred with requestIdleCallback or scheduler.yield()
  • No layout thrashing (batched reads and writes)
  • JavaScript code-split and lazy-loaded
  • Third-party scripts loaded async or deferred

CLS: Cumulative Layout Shift

CLS measures how much visible content shifts unexpectedly during the page lifecycle. Layout shifts destroy user trust -- users click the wrong button, lose their reading position, or experience visual chaos.

What causes layout shifts

Images without dimensions: When an image loads, it pushes surrounding content down if no space was reserved.

Dynamic content injection: Ads, banners, cookie notices, and lazy-loaded content that inserts above existing content.

Web fonts causing text reflow: When a web font loads and replaces a fallback font with different metrics, text reflows and shifts surrounding elements.

Dynamic content resizing: Accordions, tab panels, or carousels that change height.

CLS prevention strategies

Always set image dimensions:

<!-- Explicit dimensions reserve space -->
<img src="photo.jpg" width="800" height="600" alt="Photo">

<!-- CSS aspect-ratio works too -->
<style>
  .hero-img { aspect-ratio: 16 / 9; width: 100%; }
</style>

Reserve space for dynamic content:

/* Reserve space for an ad slot */
.ad-slot {
  min-height: 250px;
  background: #f0f0f0;
}

/* Reserve space for a cookie banner */
.cookie-banner-placeholder {
  min-height: 80px;
}

Use font-display: optional for strict CLS prevention:

@font-face {
  font-family: 'Custom Font';
  src: url('font.woff2') format('woff2');
  font-display: optional;  /* Uses font only if already cached */
}

Alternatively, use font-display: swap with font metric overrides to match fallback metrics:

@font-face {
  font-family: 'Custom Font';
  src: url('font.woff2') format('woff2');
  font-display: swap;
  size-adjust: 105%;
  ascent-override: 95%;
  descent-override: 22%;
  line-gap-override: 0%;
}

Insert dynamic content below the viewport or with explicit reservations:

// BAD: Inserting a banner at the top pushes everything down
document.body.prepend(bannerElement);

// GOOD: Use CSS transforms that don't trigger layout
// Or insert in a reserved slot with min-height already set
document.querySelector('.banner-slot').appendChild(bannerElement);

Use CSS contain for independent layout regions:

.widget {
  contain: layout;  /* Layout changes inside don't affect outside */
}
CLS optimization checklist
  • All images and videos have explicit width and height attributes
  • Ad slots and dynamic content areas have reserved minimum heights
  • Web fonts use font-display: swap or optional with metric overrides
  • No content inserted above existing visible content
  • CSS animations use transform and opacity only (composited properties)
  • Embeds (iframes, widgets) have explicit dimensions

Measuring Core Web Vitals

Lab tools (synthetic testing)
Tool Metrics Use case
Lighthouse (Chrome DevTools) LCP, TBT, CLS Development, auditing
WebPageTest All vitals + filmstrip Detailed analysis
PageSpeed Insights Lab + field data Quick overview
Field tools (real user monitoring)
Tool Data source Use case
Chrome User Experience Report (CrUX) Chrome users Public dataset, search ranking data
Google Search Console CrUX data per URL/group SEO impact monitoring
web-vitals library Your users Custom RUM implementation
Implementing RUM with the web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,  // 'good', 'needs-improvement', 'poor'
    id: metric.id,
    navigationType: metric.navigationType,
  });
  // Use sendBeacon for reliable delivery
  navigator.sendBeacon('/analytics', body);
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
Lab vs. field data
Aspect Lab (synthetic) Field (RUM)
Environment Controlled (specific device, network) Real user conditions
INP measurement Uses TBT as proxy True INP from real interactions
CLS measurement Page load only Full page lifecycle
Reproducibility High Low (varies by user)
Use for Debugging, development Monitoring, search ranking

Always prioritize field data for understanding real performance. Lab data is useful for debugging but does not capture the diversity of real-world conditions.

Performance Budgets

Performance budgets set thresholds that prevent regressions:

// Example budget in a CI/CD pipeline
const budgets = {
  lcp: 2500,      // ms
  inp: 200,       // ms (use TBT in lab)
  cls: 0.1,       // score
  ttfb: 800,      // ms
  totalJS: 300,   // KB (compressed)
  totalCSS: 50,   // KB (compressed)
  totalImages: 500, // KB
  totalFonts: 200,  // KB
};
Enforcing budgets

Lighthouse CI: Run Lighthouse in CI and fail builds that exceed budgets:

{
  "ci": {
    "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 }]
      }
    }
  }
}

Bundle size monitoring: Tools like bundlesize, size-limit, or Webpack's performance.maxAssetSize can catch JavaScript bloat before it ships.

Debugging Workflow

When Core Web Vitals are poor, follow this systematic approach:

1. Identify the problem metric

Check CrUX data in Search Console or PageSpeed Insights.

2. Reproduce in lab

Use Lighthouse or WebPageTest with throttled conditions matching your users.

3. Diagnose root cause

For poor LCP:

  • Check TTFB (server issue?)
  • Check resource waterfall (late-discovered LCP resource?)
  • Check render-blocking resources (too much blocking CSS/JS?)
  • Check resource size (unoptimized images?)

For poor INP:

  • Record a Performance trace in DevTools
  • Identify long tasks (red flags in the flame chart)
  • Find the interaction that triggered the long task
  • Determine which script/function caused the blocking

For poor CLS:

  • Use the Layout Shift Regions overlay in DevTools (Rendering panel)
  • Check for images without dimensions
  • Look for late-injected content (ads, banners)
  • Test font loading behavior
4. Fix and verify

Implement the fix, measure in lab, deploy, and verify with field data (allow 28 days for CrUX data to reflect changes).

Quick Reference: Optimization Impact

Optimization LCP INP CLS Effort
Add CDN High -- -- Low
Preload LCP resource High -- -- Low
Inline critical CSS High -- -- Medium
Code-split JavaScript Medium High -- Medium
Set image dimensions -- -- High Low
Use font-display: swap -- -- Medium Low
Defer third-party scripts Medium High Medium Low
Implement streaming SSR High Medium -- High
Break up long tasks -- High -- Medium
Reserve ad slot space -- -- High Low

Optimizing Core Web Vitals is not a one-time task -- it requires continuous monitoring and a performance-aware development culture that treats metrics regressions as bugs.

1# Core Web Vitals Optimization
2 
3Core Web Vitals are Google's standardized performance metrics that measure real user experience. They directly impact search ranking and correlate strongly with user engagement, conversion rates, and bounce rates.
4 
5 
6## Table of Contents
71. [The Three Core Web Vitals](#the-three-core-web-vitals)
82. [LCP: Largest Contentful Paint](#lcp-largest-contentful-paint)
93. [INP: Interaction to Next Paint](#inp-interaction-to-next-paint)
104. [CLS: Cumulative Layout Shift](#cls-cumulative-layout-shift)
115. [Measuring Core Web Vitals](#measuring-core-web-vitals)
126. [Performance Budgets](#performance-budgets)
137. [Debugging Workflow](#debugging-workflow)
148. [Quick Reference: Optimization Impact](#quick-reference-optimization-impact)
15 
16---
17 
18## The Three Core Web Vitals
19 
20| Metric | Measures | Good | Needs Improvement | Poor |
21|--------|----------|------|-------------------|------|
22| **LCP** (Largest Contentful Paint) | Loading performance | < 2.5s | 2.5s - 4.0s | > 4.0s |
23| **INP** (Interaction to Next Paint) | Interactivity | < 200ms | 200ms - 500ms | > 500ms |
24| **CLS** (Cumulative Layout Shift) | Visual stability | < 0.1 | 0.1 - 0.25 | > 0.25 |
25 
26### Supporting metrics
27 
28| Metric | Measures | Target |
29|--------|----------|--------|
30| **TTFB** (Time to First Byte) | Server responsiveness | < 800ms |
31| **FCP** (First Contentful Paint) | Initial render speed | < 1.8s |
32| **TBT** (Total Blocking Time) | Main thread availability (lab proxy for INP) | < 200ms |
33 
34## LCP: Largest Contentful Paint
35 
36LCP measures when the largest visible content element finishes rendering. This is what users perceive as "the page has loaded."
37 
38### What counts as the LCP element
39 
40- `<img>` elements (including those inside `<picture>`)
41- `<video>` poster images
42- Elements with `background-image` via CSS
43- Block-level text elements (`<h1>`, `<p>`, etc.)
44 
45The LCP element changes as the page loads. The final LCP element is the one that is largest when rendering stabilizes.
46 
47### Common LCP problems
48 
49**Slow server response (high TTFB):** The browser cannot render anything until it receives the first byte of HTML.
50 
51Fixes:
52- Add a CDN to reduce network latency
53- Implement server-side caching (Redis, Varnish)
54- Use streaming server-side rendering to send the HTML `<head>` immediately
55- Optimize database queries and backend processing
56- Use `103 Early Hints` to let the browser start fetching critical resources before the full response
57 
58**Late-discovered LCP resource:** The browser discovers the LCP image late in the rendering process (e.g., a CSS background image or a `<img>` tag deep in the HTML).
59 
60Fixes:
61```html
62<!-- Preload the LCP image -->
63<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
64 
65<!-- Or use fetchpriority directly -->
66<img src="/hero.webp" fetchpriority="high" alt="Hero" width="1200" height="600">
67```
68 
69**Render-blocking resources:** CSS and synchronous JavaScript delay rendering.
70 
71Fixes:
72- Inline critical CSS in `<head>`
73- Defer non-critical CSS loading
74- Use `defer` or `async` on scripts
75- Remove unused CSS (audit with Coverage panel)
76 
77**Slow resource load time:** The LCP image itself is too large or served from a slow origin.
78 
79Fixes:
80- Compress images (AVIF, WebP)
81- Serve responsive images with `srcset`
82- Use a CDN for image delivery
83- Set appropriate cache headers
84 
85**Client-side rendering:** SPAs that render content in JavaScript after the initial HTML load have inherently slow LCP.
86 
87Fixes:
88- Use server-side rendering (SSR) or static site generation (SSG)
89- Stream HTML with `Transfer-Encoding: chunked`
90- Pre-render the above-fold content on the server
91 
92### LCP optimization checklist
93 
94- [ ] TTFB under 800ms
95- [ ] LCP resource discoverable in HTML source (not CSS or JS)
96- [ ] LCP resource preloaded with `fetchpriority="high"`
97- [ ] No render-blocking resources before LCP
98- [ ] LCP image optimized (format, compression, responsive)
99- [ ] LCP image served from CDN
100- [ ] Critical CSS inlined; non-critical CSS deferred
101 
102## INP: Interaction to Next Paint
103 
104INP measures the delay between a user interaction (click, tap, keypress) and the next visual update. It replaced FID (First Input Delay) as a Core Web Vital in March 2024.
105 
106**Why INP matters more than FID:** FID only measured the delay of the first interaction. INP measures all interactions throughout the page lifecycle and reports the single worst one — except on pages with many interactions, where it drops roughly one outlier per 50 interactions (approximating the 98th percentile for high-interaction pages). A page can have a good FID but terrible INP if JavaScript blocks the main thread during later interactions.
107 
108### What causes poor INP
109 
110**Long tasks on the main thread:** Any JavaScript task that runs for more than 50ms blocks the browser from processing user input.
111 
112**Excessive event handler work:** Click handlers that perform heavy computation, DOM manipulation, or synchronous operations delay the visual response.
113 
114**Layout thrashing:** Reading layout properties (like `offsetHeight`) and then writing to the DOM in a loop forces the browser to recalculate layout repeatedly.
115 
116### INP optimization strategies
117 
118**Break up long tasks:**
119 
120```javascript
121// BAD: One long task blocking the main thread
122function processAllItems(items) {
123 items.forEach(item => heavyOperation(item));
124}
125 
126// GOOD: Yield to the main thread between chunks
127async function processAllItems(items) {
128 for (const item of items) {
129 heavyOperation(item);
130 // Yield to let the browser handle pending interactions
131 await scheduler.yield();
132 }
133}
134```
135 
136If `scheduler.yield()` is not available, use a polyfill pattern:
137 
138```javascript
139function yieldToMain() {
140 return new Promise(resolve => setTimeout(resolve, 0));
141}
142 
143async function processAllItems(items) {
144 for (let i = 0; i < items.length; i++) {
145 heavyOperation(items[i]);
146 if (i % 10 === 0) await yieldToMain();
147 }
148}
149```
150 
151**Defer non-urgent work:**
152 
153```javascript
154// Use requestIdleCallback for work that does not need to happen immediately
155requestIdleCallback(() => {
156 analytics.track('page_view');
157 prefetchNextPage();
158});
159```
160 
161**Minimize event handler work:**
162 
163```javascript
164// BAD: Heavy computation in click handler
165button.addEventListener('click', () => {
166 const result = expensiveCalculation(); // 200ms
167 updateDOM(result); // 50ms
168});
169 
170// GOOD: Show immediate feedback, defer heavy work
171button.addEventListener('click', () => {
172 showLoadingState(); // 5ms - immediate feedback
173 requestAnimationFrame(() => {
174 const result = expensiveCalculation();
175 updateDOM(result);
176 hideLoadingState();
177 });
178});
179```
180 
181**Avoid layout thrashing:**
182 
183```javascript
184// BAD: Read-write-read-write forces repeated layout
185elements.forEach(el => {
186 const height = el.offsetHeight; // Read (forces layout)
187 el.style.height = height * 2 + 'px'; // Write (invalidates layout)
188});
189 
190// GOOD: Batch reads, then batch writes
191const heights = elements.map(el => el.offsetHeight); // All reads
192elements.forEach((el, i) => {
193 el.style.height = heights[i] * 2 + 'px'; // All writes
194});
195```
196 
197**Reduce JavaScript payload:**
198- Code-split: load only what the current page needs
199- Tree-shake: remove unused exports
200- Lazy-load non-critical modules
201- Defer third-party scripts (analytics, widgets)
202 
203### INP optimization checklist
204 
205- [ ] No JavaScript tasks longer than 50ms on the main thread
206- [ ] Event handlers provide immediate visual feedback
207- [ ] Non-urgent work deferred with `requestIdleCallback` or `scheduler.yield()`
208- [ ] No layout thrashing (batched reads and writes)
209- [ ] JavaScript code-split and lazy-loaded
210- [ ] Third-party scripts loaded async or deferred
211 
212## CLS: Cumulative Layout Shift
213 
214CLS measures how much visible content shifts unexpectedly during the page lifecycle. Layout shifts destroy user trust -- users click the wrong button, lose their reading position, or experience visual chaos.
215 
216### What causes layout shifts
217 
218**Images without dimensions:** When an image loads, it pushes surrounding content down if no space was reserved.
219 
220**Dynamic content injection:** Ads, banners, cookie notices, and lazy-loaded content that inserts above existing content.
221 
222**Web fonts causing text reflow:** When a web font loads and replaces a fallback font with different metrics, text reflows and shifts surrounding elements.
223 
224**Dynamic content resizing:** Accordions, tab panels, or carousels that change height.
225 
226### CLS prevention strategies
227 
228**Always set image dimensions:**
229 
230```html
231<!-- Explicit dimensions reserve space -->
232<img src="photo.jpg" width="800" height="600" alt="Photo">
233 
234<!-- CSS aspect-ratio works too -->
235<style>
236 .hero-img { aspect-ratio: 16 / 9; width: 100%; }
237</style>
238```
239 
240**Reserve space for dynamic content:**
241 
242```css
243/* Reserve space for an ad slot */
244.ad-slot {
245 min-height: 250px;
246 background: #f0f0f0;
247}
248 
249/* Reserve space for a cookie banner */
250.cookie-banner-placeholder {
251 min-height: 80px;
252}
253```
254 
255**Use `font-display: optional` for strict CLS prevention:**
256 
257```css
258@font-face {
259 font-family: 'Custom Font';
260 src: url('font.woff2') format('woff2');
261 font-display: optional; /* Uses font only if already cached */
262}
263```
264 
265Alternatively, use `font-display: swap` with font metric overrides to match fallback metrics:
266 
267```css
268@font-face {
269 font-family: 'Custom Font';
270 src: url('font.woff2') format('woff2');
271 font-display: swap;
272 size-adjust: 105%;
273 ascent-override: 95%;
274 descent-override: 22%;
275 line-gap-override: 0%;
276}
277```
278 
279**Insert dynamic content below the viewport or with explicit reservations:**
280 
281```javascript
282// BAD: Inserting a banner at the top pushes everything down
283document.body.prepend(bannerElement);
284 
285// GOOD: Use CSS transforms that don't trigger layout
286// Or insert in a reserved slot with min-height already set
287document.querySelector('.banner-slot').appendChild(bannerElement);
288```
289 
290**Use CSS `contain` for independent layout regions:**
291 
292```css
293.widget {
294 contain: layout; /* Layout changes inside don't affect outside */
295}
296```
297 
298### CLS optimization checklist
299 
300- [ ] All images and videos have explicit width and height attributes
301- [ ] Ad slots and dynamic content areas have reserved minimum heights
302- [ ] Web fonts use `font-display: swap` or `optional` with metric overrides
303- [ ] No content inserted above existing visible content
304- [ ] CSS animations use `transform` and `opacity` only (composited properties)
305- [ ] Embeds (iframes, widgets) have explicit dimensions
306 
307## Measuring Core Web Vitals
308 
309### Lab tools (synthetic testing)
310 
311| Tool | Metrics | Use case |
312|------|---------|----------|
313| **Lighthouse** (Chrome DevTools) | LCP, TBT, CLS | Development, auditing |
314| **WebPageTest** | All vitals + filmstrip | Detailed analysis |
315| **PageSpeed Insights** | Lab + field data | Quick overview |
316 
317### Field tools (real user monitoring)
318 
319| Tool | Data source | Use case |
320|------|------------|----------|
321| **Chrome User Experience Report (CrUX)** | Chrome users | Public dataset, search ranking data |
322| **Google Search Console** | CrUX data per URL/group | SEO impact monitoring |
323| **web-vitals library** | Your users | Custom RUM implementation |
324 
325### Implementing RUM with the web-vitals library
326 
327```javascript
328import { onLCP, onINP, onCLS } from 'web-vitals';
329 
330function sendToAnalytics(metric) {
331 const body = JSON.stringify({
332 name: metric.name,
333 value: metric.value,
334 rating: metric.rating, // 'good', 'needs-improvement', 'poor'
335 id: metric.id,
336 navigationType: metric.navigationType,
337 });
338 // Use sendBeacon for reliable delivery
339 navigator.sendBeacon('/analytics', body);
340}
341 
342onLCP(sendToAnalytics);
343onINP(sendToAnalytics);
344onCLS(sendToAnalytics);
345```
346 
347### Lab vs. field data
348 
349| Aspect | Lab (synthetic) | Field (RUM) |
350|--------|----------------|-------------|
351| **Environment** | Controlled (specific device, network) | Real user conditions |
352| **INP measurement** | Uses TBT as proxy | True INP from real interactions |
353| **CLS measurement** | Page load only | Full page lifecycle |
354| **Reproducibility** | High | Low (varies by user) |
355| **Use for** | Debugging, development | Monitoring, search ranking |
356 
357**Always prioritize field data** for understanding real performance. Lab data is useful for debugging but does not capture the diversity of real-world conditions.
358 
359## Performance Budgets
360 
361Performance budgets set thresholds that prevent regressions:
362 
363```javascript
364// Example budget in a CI/CD pipeline
365const budgets = {
366 lcp: 2500, // ms
367 inp: 200, // ms (use TBT in lab)
368 cls: 0.1, // score
369 ttfb: 800, // ms
370 totalJS: 300, // KB (compressed)
371 totalCSS: 50, // KB (compressed)
372 totalImages: 500, // KB
373 totalFonts: 200, // KB
374};
375```
376 
377### Enforcing budgets
378 
379**Lighthouse CI:** Run Lighthouse in CI and fail builds that exceed budgets:
380 
381```json
382{
383 "ci": {
384 "assert": {
385 "assertions": {
386 "categories:performance": ["error", { "minScore": 0.9 }],
387 "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
388 "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
389 "total-blocking-time": ["error", { "maxNumericValue": 200 }]
390 }
391 }
392 }
393}
394```
395 
396**Bundle size monitoring:** Tools like `bundlesize`, `size-limit`, or Webpack's `performance.maxAssetSize` can catch JavaScript bloat before it ships.
397 
398## Debugging Workflow
399 
400When Core Web Vitals are poor, follow this systematic approach:
401 
402### 1. Identify the problem metric
403Check CrUX data in Search Console or PageSpeed Insights.
404 
405### 2. Reproduce in lab
406Use Lighthouse or WebPageTest with throttled conditions matching your users.
407 
408### 3. Diagnose root cause
409 
410**For poor LCP:**
411- Check TTFB (server issue?)
412- Check resource waterfall (late-discovered LCP resource?)
413- Check render-blocking resources (too much blocking CSS/JS?)
414- Check resource size (unoptimized images?)
415 
416**For poor INP:**
417- Record a Performance trace in DevTools
418- Identify long tasks (red flags in the flame chart)
419- Find the interaction that triggered the long task
420- Determine which script/function caused the blocking
421 
422**For poor CLS:**
423- Use the Layout Shift Regions overlay in DevTools (Rendering panel)
424- Check for images without dimensions
425- Look for late-injected content (ads, banners)
426- Test font loading behavior
427 
428### 4. Fix and verify
429Implement the fix, measure in lab, deploy, and verify with field data (allow 28 days for CrUX data to reflect changes).
430 
431## Quick Reference: Optimization Impact
432 
433| Optimization | LCP | INP | CLS | Effort |
434|-------------|-----|-----|-----|--------|
435| Add CDN | High | -- | -- | Low |
436| Preload LCP resource | High | -- | -- | Low |
437| Inline critical CSS | High | -- | -- | Medium |
438| Code-split JavaScript | Medium | High | -- | Medium |
439| Set image dimensions | -- | -- | High | Low |
440| Use `font-display: swap` | -- | -- | Medium | Low |
441| Defer third-party scripts | Medium | High | Medium | Low |
442| Implement streaming SSR | High | Medium | -- | High |
443| Break up long tasks | -- | High | -- | Medium |
444| Reserve ad slot space | -- | -- | High | Low |
445 
446Optimizing Core Web Vitals is not a one-time task -- it requires continuous monitoring and a performance-aware development culture that treats metrics regressions as bugs.
447 

Discussion

Alternatives

Agentic Browsing ReadinessAudit and fix agent readiness: the Lighthouse Agentic Browsing fraction, accessibility tree for agents, robots.txt and Content-Signal for AI agents, WAF treatment of agent traffic, llms.txt, Markdown delivery, ai-catalog.json, /.well-known discovery files, and WebMCP tools. Exclude AI citability and brand signals (seo-geo) and commerce protocol depth (seo-ecommerce).Marketing · MITBacklink Profile AnalysisBacklink profile analysis: referring domains, anchor text distribution, toxic link detection, competitor gap analysis. Works with free APIs (Moz, Bing Webmaster, Common Crawl) and DataForSEO extension. Use when user says backlinks, link profile, referring domains, anchor text, toxic links, link gap, link building, disavow, or backlink audit.Marketing · MIT/setup-cmsConnect a CMS to notfair SEO tools. Guides users through configuring WordPress, Strapi, Contentful, or Ghost — tests the connection, and writes credentials to .env.local. Once set up, seo-analysis automatically cross- references CMS content against Google Search Console data. Use whenever the user says "connect my CMS", "set up WordPress", "configure Strapi", "add Contentful", "connect Ghost", or "CMS setup". Also trigger if the user asks why no CMS data appears in a seo-analysis report. · MITBacklink checkBacklink profile for any domain — referring domains, authority, anchors, new/lost links, and a side-by-side vs a competitor. Use when asked "check my backlinks", "backlink profile of X", "who links to them", or "link gap vs competitor".Marketing · MIT