Core Web Vitals
Core Web Vitals optimization: LCP, INP, and CLS measurement, diagnosis, and improvement strategies for better search rankings and user experience.
Core Web Vitals are not abstract performance metrics -- they are direct measurements of user experience that Google uses as ranking signals. A page that loads its largest element in under 2.5 seconds, responds to interactions in under 200 milliseconds, and maintains visual stability during load is a page that respects the user's time and attention. Optimizing for these metrics is simultaneously optimizing for user satisfaction and search visibility. ## Key Points - Set `fetchpriority="high"` on the LCP element and `loading="lazy"` on all below-the-fold images. Never lazy-load the LCP image. - Use `content-visibility: auto` on long pages to skip rendering of off-screen sections, improving both LCP and INP by reducing initial layout and paint work. - Lazy-loading the LCP image. This is the single most common LCP regression. The LCP image must load eagerly with high fetch priority. ## Quick Example ```html <link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossorigin /> ```
skilldb get seo-content-skills/Core Web VitalsFull skill: 262 linesCore Web Vitals — SEO & Content
Core Philosophy
Core Web Vitals are not abstract performance metrics -- they are direct measurements of user experience that Google uses as ranking signals. A page that loads its largest element in under 2.5 seconds, responds to interactions in under 200 milliseconds, and maintains visual stability during load is a page that respects the user's time and attention. Optimizing for these metrics is simultaneously optimizing for user satisfaction and search visibility.
Field data from real users always takes precedence over lab data from Lighthouse. Google uses field data (CrUX) for ranking decisions, and lab results frequently diverge from real-world performance due to differences in device capabilities, network conditions, and user behavior patterns. Measure what real users experience through the web-vitals library or CrUX, and treat Lighthouse as a diagnostic tool rather than the source of truth.
The most common performance regressions are caused by well-intentioned features: lazy-loading the LCP image, inserting dynamic content that shifts layout, and loading heavy JavaScript that blocks interaction responsiveness. Understanding what each metric actually measures -- and what browser behaviors trigger regressions -- prevents introducing problems while trying to solve them.
You are an expert in Core Web Vitals optimization, covering Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS) measurement, diagnosis, and remediation.
Overview
Core Web Vitals are Google's set of real-world performance metrics that directly influence search rankings. They measure loading speed (LCP), interactivity responsiveness (INP), and visual stability (CLS). Since 2021, these metrics have been a confirmed ranking signal. Meeting the "good" thresholds improves both SEO positioning and user experience, reducing bounce rates and increasing conversions.
Core Concepts
The Three Metrics
| Metric | Measures | Good | Needs Improvement | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Time until the largest visible element renders | <= 2.5s | <= 4.0s | > 4.0s |
| INP (Interaction to Next Paint) | Latency from user input to visual update | <= 200ms | <= 500ms | > 500ms |
| CLS (Cumulative Layout Shift) | Total unexpected layout movement | <= 0.1 | <= 0.25 | > 0.25 |
LCP — What Counts as the Largest Element
LCP candidates include <img>, <video> poster images, elements with background-image via CSS, and block-level text elements (<h1>, <p>, etc.). The browser re-evaluates the LCP candidate as content loads — the final candidate when the user first interacts or the page fully loads is the reported LCP.
INP — Replacing FID
INP replaced First Input Delay (FID) in March 2024. Unlike FID which only measured the first interaction's input delay, INP measures the full latency of all interactions throughout the page lifecycle and reports the worst (or near-worst at the 98th percentile). This means long-running JavaScript that blocks any interaction — not just the first — now affects the score.
CLS — Session Windows
CLS groups layout shifts into "session windows" — bursts of shifts within 1 second, with a maximum 5-second window. The largest single session window score is the reported CLS value. Shifts caused by user interaction (clicking a button that expands content) are excluded.
Implementation Patterns
Measuring Core Web Vitals in Code
// lib/web-vitals.ts
import { onLCP, onINP, onCLS, type Metric } from "web-vitals";
function sendToAnalytics(metric: Metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // "good" | "needs-improvement" | "poor"
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
entries: metric.entries.map((e) => ({
name: (e as PerformanceEntry).name,
startTime: e.startTime,
})),
});
// Use sendBeacon for reliability on page unload
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/vitals", body);
} else {
fetch("/api/vitals", { body, method: "POST", keepalive: true });
}
}
export function reportWebVitals() {
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
}
LCP Optimization — Image Preloading
<!-- Preload the LCP image in <head> -->
<link
rel="preload"
as="image"
href="/hero-image.webp"
fetchpriority="high"
type="image/webp"
/>
// Next.js: Use priority prop on the LCP image
import Image from "next/image";
export function HeroBanner() {
return (
<Image
src="/hero-image.webp"
alt="Hero banner"
width={1200}
height={600}
priority // disables lazy loading, adds fetchpriority="high"
sizes="100vw"
/>
);
}
LCP Optimization — Font Loading
/* Prevent font-swap layout shifts and render blocking */
@font-face {
font-family: "CustomFont";
src: url("/fonts/custom.woff2") format("woff2");
font-display: swap; /* Show fallback immediately, swap when loaded */
unicode-range: U+0000-00FF; /* Only load needed character ranges */
}
<link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossorigin />
INP Optimization — Breaking Up Long Tasks
// Bad: Blocks the main thread for the entire duration
function processLargeDataset(items: DataItem[]) {
items.forEach((item) => expensiveOperation(item)); // 200ms+ blocking
updateUI();
}
// Good: Yield to the browser between chunks
async function processLargeDataset(items: DataItem[]) {
const chunkSize = 50;
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
chunk.forEach((item) => expensiveOperation(item));
// Yield to let the browser process pending interactions
await new Promise((resolve) => setTimeout(resolve, 0));
}
updateUI();
}
// Best: Use scheduler.yield() when available
async function processWithYield(items: DataItem[]) {
for (const item of items) {
expensiveOperation(item);
if ("scheduler" in globalThis && "yield" in scheduler) {
await scheduler.yield();
}
}
updateUI();
}
CLS Optimization — Reserving Space
/* Reserve space for images to prevent layout shift */
.image-container {
aspect-ratio: 16 / 9;
width: 100%;
background-color: #f0f0f0; /* placeholder color */
}
/* Reserve space for ads or dynamic embeds */
.ad-slot {
min-height: 250px; /* match expected ad height */
contain: layout; /* prevent internal changes from shifting surroundings */
}
/* Prevent font-swap CLS */
.text-content {
font-synthesis: none;
}
CLS Optimization — Dynamic Content Insertion
// Bad: Inserting a banner pushes content down
function showBanner() {
const banner = document.createElement("div");
banner.textContent = "Sale!";
document.body.prepend(banner); // Shifts everything below
}
// Good: Use CSS containment and reserved space
function showBanner() {
const slot = document.getElementById("banner-slot"); // pre-reserved space
if (slot) {
slot.textContent = "Sale!";
slot.style.visibility = "visible"; // was hidden, not display:none
}
}
Automated Monitoring with Lighthouse CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v12
with:
urls: |
http://localhost:3000/
http://localhost:3000/blog
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true
// lighthouse-budget.json
[
{
"path": "/*",
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "cumulative-layout-shift", "budget": 0.1 },
{ "metric": "interactive", "budget": 3500 }
]
}
]
Best Practices
- Measure field data (real users via CrUX or web-vitals library), not just lab data (Lighthouse). Google uses field data for ranking decisions, and lab results often differ significantly from real-world performance.
- Set
fetchpriority="high"on the LCP element andloading="lazy"on all below-the-fold images. Never lazy-load the LCP image. - Use
content-visibility: autoon long pages to skip rendering of off-screen sections, improving both LCP and INP by reducing initial layout and paint work.
Anti-Patterns
- Lazy-loading the LCP image: The single most common LCP regression. The largest contentful element must load eagerly with
fetchpriority="high". Lazy-loading it delays the metric that matters most. - Using
display: nonefor dynamic content slots: When the element later becomes visible, it causes layout shift. Usevisibility: hiddenwith pre-allocated dimensions or CSScontainto isolate the shift. - Blocking the main thread with synchronous JavaScript: Long tasks that run during page load or interaction delay both LCP and INP. Break long tasks into chunks that yield to the browser between operations.
- Ignoring font-loading CLS: Custom fonts that load asynchronously cause text reflow when they swap in, contributing to CLS. Use
font-display: swapcombined with preloading and matched fallback font metrics. - Optimizing lab scores instead of field data: Achieving a Lighthouse 100 that does not reflect real user experience. Lab conditions differ from production; always validate optimizations against CrUX field data.
Common Pitfalls
- Lazy-loading the LCP image. This is the single most common LCP regression. The LCP image must load eagerly with high fetch priority.
- Using
display: noneto hide placeholder elements for dynamic content. When the element later becomesdisplay: block, it causes layout shift. Usevisibility: hiddenwith pre-allocated dimensions instead, or use CSScontainto isolate the shift.
Install this skill directly: skilldb add seo-content-skills
Related Skills
Contentlayer
"Contentlayer and Velite for type-safe content management: transforming Markdown/MDX into typed data, schema validation, computed fields, Next.js integration, hot reload, and migration between content tools."
Fumadocs
"fumadocs documentation framework: Next.js App Router native, MDX content collections, full-text search, OpenAPI integration, TypeScript-first, customizable UI components, and content source adapters."
Mdx
"MDX authoring with Next.js: Markdown + JSX, custom components, frontmatter extraction, @next/mdx, mdx-bundler, contentlayer integration, rehype/remark plugins, and syntax highlighting with Shiki or Prism."
Next SEO
"Next.js SEO and metadata management: meta tags, Open Graph, Twitter cards, JSON-LD structured data, canonical URLs, robots directives, and sitemap generation using the Metadata API and next-seo."
Nextra
"Nextra documentation framework: MDX-powered Next.js docs and blog sites, sidebar navigation, full-text search, i18n, themes (docs and blog), frontmatter configuration, and custom components."
Programmatic SEO
Programmatic SEO strategies: generating thousands of search-optimized pages from structured data, template design, internal linking, and indexing at scale.