|$ curl https://forge-ai.dev/api/markdown?path=docs/performance/core-web-vitals
$cat docs/core-web-vitals.md
updated Recently·35 min read·published

Core Web Vitals

PerformanceIntermediate🎯Free Tools
Introduction

Core Web Vitals (CWV) are three metrics Google uses to measure real-world user experience. They directly impact SEO rankings and are the industry standard for web performance measurement.

MetricMeasuresGoodPoor
LCPLargest Contentful Paint (loading)< 2.5s> 4.0s
INPInteraction to Next Paint (responsiveness)< 200ms> 500ms
CLSCumulative Layout Shift (visual stability)< 0.1> 0.25
LCP — Largest Contentful Paint

LCP measures when the largest visible content element (hero image, heading, video poster) finishes painting. Common LCP elements: <img>, <video>, <h1>, background images.

lcp.ts
TypeScript
1// Measure LCP with web-vitals library
2import { onLCP } from "web-vitals";
3
4onLCP((metric) => {
5 console.log("LCP:", metric.value, "element:", metric.element);
6 // Send to analytics
7 sendToAnalytics({
8 metric: "LCP",
9 value: metric.value,
10 rating: metric.rating, // "good" | "needs-improvement" | "poor"
11 element: metric.element?.tagName,
12 });
13});
14
15// Optimization: preload LCP resource
16// In <head>:
17// <link rel="preload" as="image" href="/hero.jpg" fetchpriority="high" />
18
19// Optimization: use fetchpriority="high" on LCP image
20// <img src="/hero.jpg" fetchpriority="high" alt="Hero" />
21
22// Optimization: avoid lazy-loading the LCP image
23// <img src="/hero.jpg" loading="eager" alt="Hero" />
24// NOT: <img src="/hero.jpg" loading="lazy" alt="Hero" />
25
26// Optimization: use responsive images
27// <img src="/hero-800.jpg"
28// srcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
29// sizes="(max-width: 768px) 100vw, 50vw"
30// fetchpriority="high" alt="Hero" />
INP — Interaction to Next Paint

INP measures the latency of all interactions (clicks, taps, key presses) throughout the page lifecycle. It replaced FID in March 2024. INP captures the worst-case interaction latency, not just the first one.

inp.ts
TypeScript
1// Measure INP
2import { onINP } from "web-vitals";
3
4onINP((metric) => {
5 console.log("INP:", metric.value, "entries:", metric.entries.length);
6 sendToAnalytics({
7 metric: "INP",
8 value: metric.value,
9 rating: metric.rating,
10 });
11});
12
13// Optimization: break up long tasks
14// Bad: blocking main thread for 300ms
15function processLargeList(items: Item[]) {
16 for (const item of items) {
17 processItem(item); // Blocks main thread
18 }
19}
20
21// Good: use scheduler.yield() or requestIdleCallback
22async function processLargeList(items: Item[]) {
23 for (let i = 0; i < items.length; i++) {
24 processItem(items[i]);
25 // Yield to main thread every 5ms
26 if (i % 100 === 0) {
27 await new Promise((r) => setTimeout(r, 0));
28 }
29 }
30}
31
32// Optimization: use startTransition for non-urgent updates
33import { startTransition } from "react";
34
35function handleClick() {
36 startTransition(() => {
37 setFilteredResults(computeFilter()); // Non-urgent
38 });
39}
CLS — Cumulative Layout Shift
cls.ts
TypeScript
1// Measure CLS
2import { onCLS } from "web-vitals";
3
4onCLS((metric) => {
5 console.log("CLS:", metric.value);
6 sendToAnalytics({ metric: "CLS", value: metric.value, rating: metric.rating });
7});
8
9// Fix: always set width/height on images
10// <img src="photo.jpg" width="800" height="600" alt="..." />
11// CSS: img { aspect-ratio: 800/600; }
12
13// Fix: reserve space for ads/embeds
14// .ad-container { min-height: 250px; }
15
16// Fix: use CSS aspect-ratio instead of padding hacks
17// .video-wrapper { aspect-ratio: 16/9; width: 100%; }
18
19// Fix: use font-display: optional for web fonts
20// @font-face {
21// font-family: 'CustomFont';
22// src: url('/font.woff2') format('woff2');
23// font-display: optional; // No layout shift — uses fallback if font isn't ready
24// }
25
26// Fix: avoid inserting content above existing content
27// Use skeleton screens or reserve space

best practice

Always specify width and height on <img> and <video> elements. The browser uses these to calculate aspect ratio before the image loads, preventing layout shifts.
Field vs Lab Data
TypeSourceUse For
Field (RUM)Real users, real devicesProduction monitoring, SEO impact
LabSimulated environment (Lighthouse)Development, debugging, CI
field-vs-lab.sh
Bash
1# Lab testing with Lighthouse CLI
2npx lighthouse https://example.com --output=json --output-path=./report.json
3
4# CI integration
5npx lighthouse-ci autorun --config=./lighthouserc.json
6
7# lighthouserc.json:
8# { "ci": { "collect": { "url": ["http://localhost:3000"] },
9# "assert": { "assertions": { "categories:performance": ["error", { "minScore": 0.9 }] } } } }
10
11# Field data: Chrome UX Report (CrUX)
12# https://developer.chrome.com/docs/crux/ — real user data
13# Also available via PageSpeed Insights API
$Blueprint — Engineering Documentation·Section ID: PERF-CWV-01·Revision: 1.0