Optimize the three Core Web Vitals that Google uses for search ranking and user experience. Covers Largest Contentful Paint, Cumulative Layout Shift, Interaction to Next Paint, measurement tools, and the performance engineering patterns that deliver fast, stable, responsive pages.
This reference page covers the optimization of the three Core Web Vitals metrics—Largest Contentful Paint, Interaction to Next Paint, and Cumulative Layout Shift—as defined by the web community and used in search ranking and user experience benchmarks. It matters when you are measuring real-user experience metrics, preparing for a performance budget, debugging a regression in the field, or ensuring a new deployment does not trigger a “Poor” rating in search consoles. The following sections are organized by the specific task the reader is attempting, avoiding a taxonomic breakdown of metric definitions in favor of practical, goal-oriented guidance.
Before optimizing, you must capture accurate field and lab data. The standard tool for this is the web-vitals JavaScript library, which can be run from the command line or injected into a page.
npx web-vitals@2 log-console
This command instruments the page and prints each metric interaction to the console in the format CLS: 0.1, FID: 50, LCP: 2400. The output includes the timestamp, the metric name, and the value in milliseconds or dimensionless units.
For integration with Search Console or PageSpeed Insights, the same library can be sent to an endpoint:
npx web-vitals@2 send-to-ga --google-analytics-id=G-XXXXXXX
If you are working in a lab environment, the metrics are also available via the Performance API. The following snippet captures the metrics and logs them on page load:
import { getLCP, getFID, getCLS } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
href: location.href,
metricType: metric.name,
value: metric.value,
delta: metric.delta,
id: metric.id,
});
// Replace with your actual analytics endpoint
navigator.sendBeacon('https://example.com/collect', body);
}
getLCP(sendToAnalytics);
getFID(sendToAnalytics);
getCLS(sendToAnalytics);
A metric is considered "Good" if it falls within the published thresholds: LCP ≤ 2.5s, INP ≤ 200ms, and CLS ≤ 0.1. Values exceeding these thresholds indicate a failure that requires intervention.
LCP measures the render time of the largest image or text block visible within the viewport. A high LCP typically indicates that the critical rendering resource is discovering too late or blocking too long.
Preload critical assets If the LCP element is an image or font required above the fold, add a preload link in the document head. The fetchpriority="high" attribute overrides the default low priority and signals the browser to fetch this resource early.
<link rel="preload" href="/hero-image.webp" as="image" fetchpriority="high">
Ensure the as="image" attribute matches the resource type; mismatched as values prevent the browser from applying the high priority correctly.
Optimize CSS delivery Render-blocking CSS delays LCP. Inline critical CSS required for above-the-fold content and defer the remainder. If using a build system, configure it to extract and inline the critical path CSS.
# Example using a hypothetical build plugin configuration key
# This pattern applies to tools like Critical CSS extractors
critical-css:
src: 'dist/styles.css'
dest: 'dist/critical.css'
inlinedSrc: 'index.html'
width: 1200
height: 800
Image format and sizing Serve LCP images in modern formats (WebP or AVIF) and specify explicit width and height attributes. While CLS is addressed separately, missing dimensions on the LCP image can contribute to layout instability during load.
<img src="/hero-image.webp" width="1200" height="600" alt="Hero section illustration">
INP replaced First Input Delay in March 2024. It measures the responsiveness of a page to user interaction, capturing the longest interaction latency across the page lifespan. A high INP indicates main-thread contention.
Break up long tasks JavaScript execution blocks the main thread. If a task exceeds 50ms, it degrades INP. Use scheduler.yield() or requestIdleCallback to offload non-urgent work.
// Break a heavy loop into chunks
function processItems(items) {
let index = 0;
function step() {
for (; index < items.length; index++) {
if (index > 50) {
// Yield control back to the browser
scheduler.yield();
return;
}
// process item
}
}
requestIdleCallback(step);
}
If scheduler.yield() is unavailable in the target runtime, setTimeout(() => {}, 0) inside a loop is a fallback, though it is less efficient.
Debounce and throttle event handlers Scroll, resize, and input events can fire rapidly and accumulate work on the main thread. Apply debouncing or throttling to limit the rate of execution.
// Simple debounce implementation
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Usage
const handleScroll = debounce(() => {
// expensive layout read/reflow
}, 250);
window.addEventListener('scroll', handleScroll);
Web Workers for heavy computation If the INP failure is caused by a specific heavy computation (e.g., image processing, large data parsing), move that logic off the main thread using a Web Worker.
// main.js
const worker = new Worker('processor.worker.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
console.log('Result:', e.data);
};
processor.worker.js
self.onmessage = (e) => {
// Heavy processing here, no DOM access
const result = e.data.data.reduce((a, b) => a + b, 0);
self.postMessage(result);
};
CLS measures the sum total of all individual layout shift scores. A shift occurs whenever a visible element changes its position in the viewport. The primary cause is content loading without reserved space.
Provide explicit dimensions for media Every img, video, iframe, and canvas element should include width and height attributes, or equivalent CSS width/height that computes to a fixed dimension. This reserves the exact space in the layout before the resource loads.
<img src="/product-photo.webp" width="800" height="450" alt="Product image">
If dimensions are unknown (e.g., responsive images), use CSS aspect-ratio boxes to maintain proportional space.
.aspect-box {
width: 100%;
aspect-ratio: 16 / 9;
}
Avoid inserting content above existing content Do not inject DOM elements (toasts, modals, banners) above the fold without user interaction. If a notification must appear, ensure it is appended at the bottom of the container or triggered by a deliberate user action (click).
Optimize web font loading FOUT (Flash of Unstyled Text) and FOSUB (Flash of Styled Untext) can trigger layout shifts if the font swap occurs mid-render. Use font-display: swap in your CSS to ensure text remains visible immediately, even if the fallback system font is used temporarily.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
}
Additionally, preconnect to the font provider and preload critical font weights to reduce the time between text rendering and font loading.
<link rel="preconnect" href="https://fonts.example.com">
<link rel="preload" href="/fonts/inter-bold.woff2" as="font" type="font/woff2" crossorigin>
Performance budgets should be enforced in the continuous integration pipeline to prevent Core Web Vitals from degrading unnoticed.
Lighthouse CI Lighthouse Continuous Integration (lhci) can be configured to run audits on every pull request and fail the build if metrics exceed defined thresholds.
# Initialize Lighthouse CI
npx @lhci/[email protected] init
# Run a build and upload the results
npx @lhci/cli collect --url=http://localhost:3000 --upload=false
# Assert thresholds (configured in .lighthouserc.js)
# Example .lighthouserc.js content
module.exports = {
ci: {
collect: {
urls: ['http://localhost:3000'],
assert: {
assertions: {
'cwv': {
'max-score': 1,
'min-perf': ['2.5s', '200ms', '0.1']
}
}
}
}
}
};
If the build fails, the CI output will report which metric breached the threshold and the specific URL, allowing for rapid triage.
Several Gotchas are only learned after a metric degrades in production.
The INP/FID transition Because INP measures the *longest* interaction across the entire page lifespan, a page might have a good FID (first interaction) but a poor INP if a later interaction—such as a modal opening or a form submission—causes a long main-thread freeze. Always profile the full session, not just the initial click.
CLS gotchas with iframes Layout shifts frequently originate from third-party iframes (ads, widgets) that resize after initial render. The sandbox attribute with allow-scripts can mitigate some shifts, but the most reliable fix is ensuring the iframe element has explicit dimensions or using the loading="lazy" attribute for below-the-fold iframes to defer their layout impact.
Preload vs Preconnect confusion Both are network hints, but they serve different purposes. preconnect establishes the early connection (DNS, TLS, TCP) but does not fetch the resource. preload fetches the resource early. Applying preload to non-critical resources can actually hurt performance by competing for bandwidth with the LCP candidate. Reserve preload for the actual LCP element or critical CSS; use preconnect for origins hosting those resources.
Misinterpreting "Good" thresholds in CI A "Good" score in Lab data (Chrome DevTools Lighthouse) does not guarantee a "Good" field score. Lab environments throttle CPU and network, often masking main-thread jank that manifests under real-user conditions. Always correlate lab results with field data from Search Console or Real User Monitoring before declaring an optimization complete.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy