Introduction
Google's Core Web Vitals evolved significantly since their introduction. The biggest change: Interaction to Next Paint (INP) replaced First Input Delay (FID) as a Core Web Vital in March 2024. INP provides a more comprehensive measure of interactivity by considering all interactions throughout the page lifecycle, not just the first input. This guide covers the current state of Core Web Vitals with practical measurement and optimization strategies.
Understanding the Current Core Web Vitals
Largest Contentful Paint (LCP)
LCP measures when the largest content element in the viewport becomes visible. It captures the user's perception of loading speed.
LCP elements are typically:
- Hero images or banner images
- Large text blocks (headlines, paragraphs)
- Video poster images
- Background images loaded via CSS
// Measure LCP with the web-vitals library
import { onLCP } from 'web-vitals';
onLCP(({ value, entries }) => {
console.log(`LCP: ${value}ms`);
console.log(`Element:`, entries[entries.length - 1].element);
});Thresholds:
- Good: ≤ 2.5 seconds
- Needs Improvement: 2.5 – 4.0 seconds
- Poor: > 4.0 seconds
Cumulative Layout Shift (CLS)
CLS measures visual stability by quantifying unexpected layout shifts. It's calculated from two components: impact fraction and distance fraction.
Layout Shift Score = Impact Fraction Ă— Distance Fraction
- Impact fraction: How much of the viewport is affected by the shift
- Distance fraction: How far the unstable elements moved
import { onCLS } from 'web-vitals';
onCLS(({ value, entries }) => {
console.log(`CLS: ${value}`);
entries.forEach(entry => {
console.log(`Shifted elements:`, entry.sources);
});
});Thresholds:
- Good: ≤ 0.1
- Needs Improvement: 0.1 – 0.25
- Poor: > 0.25
Interaction to Next Paint (INP)
INP measures responsiveness by observing the latency of all click, tap, and keyboard interactions throughout the page lifecycle. The INP value is the worst interaction latency (excluding outliers).
import { onINP } from 'web-vitals';
onINP(({ value, entries }) => {
console.log(`INP: ${value}ms`);
entries.forEach(entry => {
console.log(`Interaction: ${entry.name}`);
console.log(`Input delay: ${entry.processingStart - entry.startTime}ms`);
console.log(`Processing time: ${entry.processingEnd - entry.processingStart}ms`);
console.log(`Presentation delay: ${entry.startTime + entry.duration - entry.processingEnd}ms`);
});
});INP breaks down into three phases:
- Input delay: Time from interaction to event handler start
- Processing time: Time to execute event handlers
- Presentation delay: Time from handler completion to next frame
Thresholds:
- Good: ≤ 200ms
- Needs Improvement: 200 – 500ms
- Poor: > 500ms
Step-by-Step Measurement Setup
Step 1: Install web-vitals
npm install web-vitalsStep 2: Create a Reporting Endpoint
// api/vitals.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const metric = req.body;
// Store in your database or send to analytics service
console.log(`[${metric.name}] ${metric.value}ms`);
// Send to Google Analytics 4
if (typeof gtag !== 'undefined') {
gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_category: 'Web Vitals',
event_label: metric.id,
non_interaction: true,
});
}
res.status(200).json({ ok: true });
}Step 3: Instrument Your Application
// lib/vitals.ts
import { onLCP, onFID, onCLS, onINP } from 'web-vitals';
type VitalMetric = {
name: string;
value: number;
id: string;
delta: number;
rating: 'good' | 'needs-improvement' | 'poor';
};
function sendMetric(metric: VitalMetric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
delta: metric.delta,
rating: metric.rating,
page: window.location.pathname,
userAgent: navigator.userAgent,
connection: (navigator as any).connection?.effectiveType,
});
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', body);
} else {
fetch('/api/vitals', { body, method: 'POST', keepalive: true });
}
}
export function initVitals() {
onLCP(sendMetric);
onFID(sendMetric);
onCLS(sendMetric);
onINP(sendMetric);
}Step 4: Visualize Data
// Build a dashboard showing p75 values
SELECT
name,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY value) as p75,
COUNT(*) as sample_count
FROM web_vitals
WHERE created_at > NOW() - INTERVAL '7 days'
GROUP BY name;Optimization Strategies
LCP Optimization
<!-- Preload the LCP image -->
<link rel="preload" as="image" href="/hero.avif" type="image/avif">
<!-- Use fetchpriority for the LCP image -->
<img src="/hero.avif" fetchpriority="high" alt="Hero" width="1200" height="600">
<!-- Inline critical CSS -->
<style>
/* Critical above-the-fold styles */
.hero { display: flex; align-items: center; }
</style>
<!-- Defer non-critical CSS -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">CLS Optimization
/* Set explicit dimensions */
img, video, iframe {
aspect-ratio: attr(width) / attr(height);
width: 100%;
height: auto;
}
/* Modern approach using aspect-ratio */
.hero-image {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
}
/* Reserve space for dynamic content */
.skeleton {
min-height: 200px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}INP Optimization
// Use scheduler.yield() to break up long tasks
async function handleInteraction(data) {
// Phase 1: Immediate visual feedback
showLoadingState();
// Phase 2: Process data in chunks
await scheduler.yield();
const processed = await processData(data);
// Phase 3: Update DOM
await scheduler.yield();
updateUI(processed);
}
// Use requestIdleCallback for non-urgent work
function scheduleAnalytics(event) {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => sendAnalytics(event));
} else {
setTimeout(() => sendAnalytics(event), 0);
}
}
// Avoid layout thrashing
function updateElements(items) {
// Batch reads
const heights = items.map(el => el.getBoundingClientRect().height);
// Batch writes
items.forEach((el, i) => {
el.style.height = `${heights[i] * 2}px`;
});
}Advanced Monitoring
Chrome UX Report (CrUX)
// Fetch CrUX data for your origin
const response = await fetch('https://chromeuxreport.googleapis.com/v1/records:queryRecord', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
origin: 'https://your-site.com',
metrics: ['largest_contentful_paint', 'cumulative_layout_shift', 'interaction_to_next_paint'],
}),
});
const data = await response.json();
// data.record.metrics contains p75 values for each metricPerformanceObserver for Custom Metrics
// Track Total Blocking Time (TBT) as a proxy for INP in lab
let tbt = 0;
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 50) {
tbt += entry.duration - 50;
}
});
});
observer.observe({ type: 'longtask', buffered: true });Best Practices
- Monitor both lab and field data: Use Lighthouse for development and CrUX/RUM for production
- Set performance budgets: Define thresholds and enforce them in CI/CD
- Prioritize INP: It's the newest Core Web Vital and often the hardest to optimize
- Test on real devices: Emulated throttling doesn't capture all real-world issues
- Use the web-vitals library: It handles edge cases and provides accurate measurements
Common Pitfalls
| Pitfall | Impact | Solution |
|---|---|---|
| Ignoring INP | Poor interactivity scores | Measure and optimize all interactions |
| CLS from web fonts | Layout shifts during font swap | Use font-display: optional or size-adjust |
| LCP from client rendering | Delayed largest content | SSR or preload critical content |
| No mobile testing | Mobile often has worse vitals | Test on real mobile devices |
Metric Thresholds and Scoring
Understanding the thresholds Google uses for each Core Web Vital helps you set appropriate performance budgets for your team. The good threshold represents the target that at least seventy-five percent of your page loads should achieve. The needs improvement range indicates pages that are borderline and should be prioritized for optimization. The poor threshold indicates pages that are significantly underperforming and need immediate attention.
| Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
| LCP | ≤ 2.5s | 2.5s – 4.0s | > 4.0s |
| INP | ≤ 200ms | 200ms – 500ms | > 500ms |
| CLS | ≤ 0.1 | 0.1 – 0.25 | > 0.25 |
Google evaluates these thresholds at the seventy-fifth percentile of page loads, meaning that seventy-five percent of your users should experience metrics in the good range. Focus your optimization efforts on the metric that is furthest from the good threshold first, as this will have the largest impact on your overall page experience score.
Interaction to Next Paint
Interaction to Next Paint is the newest Core Web Vital, replacing First Input Delay in March twenty twenty four. While First Input Delay only measured the delay before the first interaction, INP measures the responsiveness of all interactions throughout the page lifecycle. INP is the latency of the slowest interaction, excluding outliers. An INP below two hundred milliseconds is considered good. Optimize INP by breaking long tasks into smaller chunks using requestIdleCallback or scheduler.yield. Avoid blocking the main thread during event handlers. Use the Web Vitals JavaScript library to measure INP in the field and report it to your analytics platform.
Measuring Performance
Accurate measurement of Core Web Vitals requires both field and lab data collection. Use the web-vitals JavaScript library to measure metrics in the field and send them to your analytics platform. Configure the library to report metrics at the appropriate time: report LCP when the largest contentful paint element is identified, report CLS when the page is hidden, and report INP when the page is hidden. Use Lighthouse in CI to measure lab data and catch regressions before deployment. Combine field and lab data to get a complete picture of your application's performance across different devices, networks, and user behaviors.
Real User Monitoring
Real User Monitoring captures performance data from actual users visiting your application. Implement RUM by including the web-vitals library in your production build and sending metrics to your analytics backend. Segment your data by device type, geographic location, connection speed, and browser to identify specific groups of users who are experiencing poor performance. Use this data to prioritize optimizations that will have the greatest impact on your user base. Set up dashboards that show the distribution of each metric across your users, not just the median, because the seventy-fifth percentile is the threshold that Google uses for the good classification.
Common Performance Anti-Patterns
Avoid these common performance anti-patterns that degrade Core Web Vitals. Loading large JavaScript bundles that block rendering increases LCP. Using document.write to inject scripts blocks the parser and delays LCP. Dynamically injecting content above existing content causes layout shifts that increase CLS. Using web fonts without font-display swap causes invisible text that degrades CLS when the font loads. Running long synchronous tasks in event handlers blocks the main thread and increases INP. Fetching all data upfront instead of lazily loading what is needed wastes bandwidth and delays LCP. Identifying and fixing these anti-patterns often provides the largest performance improvements.
Performance Monitoring Tools
Several tools help you monitor Core Web Vitals in production. Google PageSpeed Insights provides both lab and field data for specific URLs. Google Search Console shows field data for your entire domain with trends over time. The Chrome UX Report provides aggregated field data for millions of websites. Lighthouse CI integrates performance checks into your CI pipeline. The web-vitals library measures metrics in the field and sends them to your analytics platform. Chrome DevTools Performance tab captures detailed performance profiles for debugging. Use a combination of these tools to get a complete picture of your application's performance across different environments.
Resource Loading Optimization
Optimize resource loading to improve Core Web Vitals. Use preload links for critical resources like fonts, stylesheets, and hero images. Implement resource hints like dns-prefetch and preconnect for third-party domains. Use modern image formats like WebP and AVIF for better compression. Implement responsive images with srcset and sizes to serve appropriately sized images for each device. Defer non-critical JavaScript using the defer or async attributes. Inline critical CSS to eliminate render-blocking requests. Bundle and tree-shake your JavaScript to reduce total download size. These resource loading optimizations directly impact LCP and improve the overall loading experience.
Server-Side Optimization
Server-side performance directly impacts Core Web Vitals. Reduce server response times by optimizing database queries, implementing caching, and using a CDN. Use server-side rendering to deliver pre-rendered HTML that the browser can display immediately. Implement streaming SSR to send parts of the page as they become ready. Configure HTTP/2 or HTTP/3 to enable multiplexed resource loading. Set appropriate cache headers to enable browser and CDN caching. These server-side optimizations reduce the time between the initial request and the first meaningful content appearing on screen, directly improving LCP.
Performance Monitoring in Production
Setting up comprehensive performance monitoring ensures that your optimizations continue to deliver value after deployment. Without monitoring, performance regressions can silently accumulate as your application evolves, eventually degrading user experience below acceptable thresholds.
Real User Monitoring (RUM)
Real User Monitoring captures performance metrics from actual users in production environments, providing data that synthetic benchmarks cannot replicate. Implement RUM by collecting Core Web Vitals metrics from the web-vitals library and sending them to your analytics platform:
import { onCLS, onFID, onLCP, onINP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
page: window.location.pathname,
connection: navigator.connection?.effectiveType,
deviceMemory: navigator.deviceMemory,
});
// Use Beacon API for reliable delivery even during page unload
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', body);
} else {
fetch('/api/vitals', { body, method: 'POST', keepalive: true });
}
}
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onTTFB(sendToAnalytics);Performance Budgets
Establish performance budgets that prevent regressions from reaching production. Configure your CI pipeline to fail builds that exceed these budgets:
{
"budgets": [
{
"type": "initial",
"maximumWarning": "200kb",
"maximumError": "250kb"
},
{
"type": "bundle",
"name": "vendor",
"maximumWarning": "150kb",
"maximumError": "200kb"
}
]
}Track bundle size changes in pull requests using tools like bundlewatch or size-limit. These tools compare the bundle size of the current branch against the base branch and report differences directly in the PR, making it easy to identify which changes introduced significant size increases.
Continuous Performance Regression Testing
Integrate Lighthouse CI into your deployment pipeline to catch performance regressions before they reach production. Configure it to run against key pages and fail the build if any metric drops below your defined thresholds:
# lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/', 'http://localhost:3000/dashboard'],
numberOfRuns: 3,
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.95 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
},
},
},
};This automated approach ensures that every deployment maintains your performance standards, preventing the gradual degradation that occurs when performance is only manually tested.
Community Resources and Further Learning
The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.
Curated Learning Pathways
Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.
Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.
Contributing to Open Source
Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.
# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
# Run the project's contribution setup
npm run setup:dev
npm run test # Ensure tests pass before making changes
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
Closes #1234"
git push origin fix/issue-descriptionBuilding a Technical Knowledge Base
Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.
Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.
Staying Current with Industry Trends
Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.
Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.
Mentorship and Knowledge Sharing
Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.
Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.
Conclusion
Core Web Vitals—LCP, CLS, and INP—represent the most important aspects of user experience on the web. With INP replacing FID, the focus has shifted from just the first interaction to all interactions throughout the page lifecycle. By measuring, monitoring, and optimizing these metrics, you can build websites that are fast, stable, and responsive.
Key takeaways:
- LCP measures loading—optimize critical rendering path and resource loading
- CLS measures visual stability—set dimensions, reserve space, use skeleton screens
- INP measures interactivity—break up long tasks, yield to main thread, minimize JS execution
- Use web-vitals library for accurate, production-ready measurement
- Monitor with CrUX and RUM to understand real user experience