Core Web Vitals
Understanding and optimizing Google's Core Web Vitals metrics - LCP, INP, and CLS
Core Web Vitals
Core Web Vitals are a set of real-world user experience metrics that Google uses to measure the quality of a web page. They're part of Google's Page Experience ranking factors and directly impact your SEO rankings.
What Are Core Web Vitals?
Core Web Vitals measure three aspects of user experience:
- Loading Performance - How fast content appears
- Interactivity - How quickly the page responds to user input
- Visual Stability - How stable the page is as it loads
These metrics are measured using real user data (Real User Monitoring - RUM), not synthetic tests. Google collects data from Chrome users who have opted into sharing usage statistics.
Why Core Web Vitals Matter
SEO Impact
- Search Ranking: Core Web Vitals are ranking factors in Google Search
- Page Experience Signal: Part of Google's Page Experience update
- Mobile-First: Especially important for mobile search rankings
User Experience Impact
- Bounce Rate: Poor Core Web Vitals = higher bounce rate
- Engagement: Fast, responsive sites = better user engagement
- Conversions: Better performance = higher conversion rates
- User Satisfaction: Users expect fast, smooth experiences
Business Impact
Good Core Web Vitals:
✅ Better SEO rankings
✅ Lower bounce rate
✅ Higher conversions
✅ Better user satisfaction
✅ Competitive advantageThe Core Web Vitals
1. Largest Contentful Paint (LCP)
What it measures: Loading performance - how long it takes for the largest content element to be visible.
Target: < 2.5 seconds
Needs improvement: 2.5 - 4.0 seconds
Poor: > 4.0 seconds
Understanding LCP
LCP measures the time from when the page starts loading until the largest content element in the viewport is rendered. This is usually:
- Hero images (most common)
- Large text blocks (headings, paragraphs)
- Hero videos (with poster image)
- Background images (with text overlay)
How LCP is Calculated
LCP = Time when largest content element is rendered
- Time when page navigation startedThe browser identifies the largest element by:
- Measuring all visible elements
- Finding the largest one (by area)
- Recording when it finishes rendering
What Affects LCP
Server Response Time:
- Slow server = slow LCP
- Server-side rendering helps
- CDN reduces latency
- Target: < 600ms TTFB (Time to First Byte)
Resource Load Time:
- Large images slow LCP
- Render-blocking CSS/JS delays LCP
- Font loading can delay text LCP
Client-Side Rendering:
- JavaScript execution delays LCP
- Heavy frameworks increase LCP
- Client-side routing affects LCP
Example LCP Timeline
0ms → Page navigation starts
100ms → HTML arrives
200ms → CSS starts loading
400ms → CSS finishes, rendering starts
800ms → Hero image starts loading
1200ms → Hero image finishes loading
1200ms → LCP! (Hero image is largest element)Common LCP Elements
// Hero image (most common)
<img src="/hero.jpg" alt="Hero" />
// Large text block
<h1>Welcome to our site</h1>
// Hero video
<video poster="/hero-poster.jpg">
<source src="/hero.mp4" />
</video>Optimizing LCP
1. Identify LCP Element
// Find LCP element
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP element:', lastEntry.element);
console.log('LCP time:', lastEntry.renderTime || lastEntry.loadTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });2. Prioritize LCP Resource
// Next.js Image with priority
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // Loads immediately, no lazy loading
quality={85}
/>3. Preload LCP Image
<head>
<!-- Preload hero image -->
<link rel="preload" as="image" href="/hero.webp" />
</head>4. Optimize Image Size
// Responsive images
<Image
src="/hero.jpg"
srcSet="/hero-640w.webp 640w,
/hero-1280w.webp 1280w,
/hero-1920w.webp 1920w"
sizes="100vw"
alt="Hero"
/>5. Use Modern Formats
<picture>
<source srcSet="/hero.avif" type="image/avif" />
<source srcSet="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero" />
</picture>6. Eliminate Render-Blocking Resources
<!-- Inline critical CSS -->
<style>
.hero { height: 100vh; background: #000; }
</style>
<!-- Defer non-critical CSS -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">7. Use CDN
// Serve images from CDN
<Image
src="https://cdn.example.com/hero.jpg"
alt="Hero"
priority
/>8. Improve Server Response Time
Target: < 600ms TTFB (Time to First Byte)
Strategies:
- Use CDN
- Enable compression
- Optimize database queries
- Use caching
- Use HTTP/22. Interaction to Next Paint (INP)
What it measures: Interactivity - how quickly the page responds to user input.
INP Target: < 200ms
Needs improvement: 200 - 500ms
Poor: > 500ms
Note: INP replaced FID (First Input Delay) in 2024. FID only measured the first interaction, while INP measures all interactions.
Understanding INP
INP measures interactivity - how quickly the page responds to user input. It's the successor to FID (First Input Delay), which was deprecated in 2024.
INP measures:
- Click events
- Keyboard events (typing, keydown)
- Tap events (mobile)
INP does NOT measure:
- Scroll events
- Hover events
- Passive interactions
How INP is Calculated
INP = Time from user interaction to next paint (visual feedback)
Total INP = Input Delay + Processing Time + Presentation DelayThe browser measures:
- Input delay: Time from interaction to event handler starts
- Processing time: Time event handler takes to execute
- Presentation delay: Time from handler finish to next paint
What Affects INP
Long Tasks:
- Heavy JavaScript execution blocks main thread
- Large bundle sizes increase parse/execute time
- Third-party scripts add overhead
Event Handler Performance:
- Slow event handlers increase INP
- Complex calculations in handlers
- Synchronous operations block interaction
Main Thread Blocking:
- Layout thrashing
- Style recalculations
- Paint operations
Example INP Timeline
0ms → User clicks button
50ms → Input delay (main thread busy)
50ms → Event handler starts
150ms → Event handler finishes
160ms → Browser paints update
160ms → INP = 160ms ✅ (Good!)FID vs INP
FID (Deprecated):
- Only measured first interaction
- Only measured delay (not processing)
- Limited scope
INP (Current):
- Measures all interactions
- Measures full interaction lifecycle
- Better reflects real user experience
- More comprehensive
Optimizing INP
1. Identify Long Tasks
// Monitor long tasks (> 50ms)
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 50) {
console.log('Long task:', {
duration: entry.duration,
startTime: entry.startTime,
});
}
});
}).observe({ type: 'longtask', buffered: true });2. Break Up Long Tasks
// ❌ BAD: Long synchronous task
function processData(data: any[]) {
data.forEach(item => {
// Heavy processing
heavyCalculation(item);
});
}
// ✅ GOOD: Break into smaller chunks
async function processData(data: any[]) {
const chunks = chunkArray(data, 100);
for (const chunk of chunks) {
await new Promise(resolve => setTimeout(resolve, 0));
chunk.forEach(item => {
heavyCalculation(item);
});
}
}3. Use Web Workers
// heavy-calculation.worker.ts
self.addEventListener('message', (e) => {
const result = heavyCalculation(e.data);
self.postMessage(result);
});
// Main thread
const worker = new Worker(new URL('./heavy-calculation.worker', import.meta.url));
worker.postMessage(data);
worker.onmessage = (e) => {
console.log('Result:', e.data);
};4. Debounce/Throttle Input Handlers
import { debounce, throttle } from 'lodash';
// Debounce search (wait for user to stop typing)
const handleSearch = debounce((query: string) => {
// Expensive search
performSearch(query);
}, 300);
// Throttle scroll (limit frequency)
const handleScroll = throttle(() => {
// Update scroll position
updateScrollPosition();
}, 100);5. Defer Non-Critical JavaScript
import { lazy, Suspense } from 'react';
// Load heavy component only when needed
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}6. Use requestIdleCallback
// Run non-critical work when browser is idle
requestIdleCallback(() => {
// Non-critical analytics
trackAnalytics();
}, { timeout: 2000 });7. Optimize Third-Party Scripts
// Load third-parties after page interactive
<Script
src="https://third-party.com/script.js"
strategy="afterInteractive"
/>3. Cumulative Layout Shift (CLS)
What it measures: Visual stability - how much the page shifts during loading.
Target: < 0.1
Needs improvement: 0.1 - 0.25
Poor: > 0.25
Understanding CLS
CLS measures unexpected layout shifts - when elements move around as the page loads. This creates a jarring user experience.
CLS measures:
- Elements that shift position
- Elements that change size
- Elements that appear/disappear
CLS does NOT measure:
- User-initiated shifts (scrolling, clicking)
- Expected animations
- Transitions
How CLS is Calculated
CLS = Sum of all layout shift scores
Layout Shift Score = Impact Fraction × Distance Fraction
Impact Fraction = Area of viewport affected by shift
Distance Fraction = Distance element moved (as % of viewport)Example:
Element shifts:
- Impact Fraction: 0.5 (50% of viewport)
- Distance Fraction: 0.3 (moved 30% of viewport height)
Layout Shift Score = 0.5 × 0.3 = 0.15
If this happens 3 times:
CLS = 0.15 + 0.10 + 0.08 = 0.33 ❌ (Poor!)What Causes Layout Shifts
Images Without Dimensions:
<!-- ❌ BAD: Causes layout shift -->
<img src="/image.jpg" alt="Image" />
<!-- ✅ GOOD: Prevents layout shift -->
<img src="/image.jpg" alt="Image" width="800" height="600" />Fonts Loading:
/* ❌ BAD: Text shifts when font loads */
.text {
font-family: 'Custom Font', sans-serif;
}
/* ✅ GOOD: Reserve space with font-display */
@font-face {
font-family: 'Custom Font';
font-display: swap; /* Prevents invisible text */
}Dynamic Content:
// ❌ BAD: Content appears, shifts layout
{data && <div>{data.content}</div>}
// ✅ GOOD: Reserve space with skeleton
{data ? (
<div>{data.content}</div>
) : (
<div style={{ height: '200px' }}>Loading...</div>
)}Ads/Embeds:
<!-- ❌ BAD: Ad loads, shifts content -->
<div id="ad-container"></div>
<!-- ✅ GOOD: Reserve space for ad -->
<div id="ad-container" style="min-height: 250px;">
<div>Loading ad...</div>
</div>Common CLS Issues
- Images without width/height
- Fonts without font-display: swap
- Ads/embeds without reserved space
- Dynamically inserted content
- Animations that trigger layout
Optimizing CLS
1. Measure CLS
let clsScore = 0;
new PerformanceObserver((list) => {
list.getEntries().forEach((entry: any) => {
if (!entry.hadRecentInput) {
clsScore += entry.value;
console.log('Layout shift:', entry.value);
console.log('Total CLS:', clsScore);
}
});
}).observe({ type: 'layout-shift', buffered: true });2. Set Dimensions on Images/Videos
// ❌ BAD: No dimensions (causes layout shift)
<img src="/image.jpg" alt="Image" />
// ✅ GOOD: Explicit dimensions
<img src="/image.jpg" alt="Image" width="800" height="600" />
// ✅ GOOD: Next.js Image
<Image
src="/image.jpg"
alt="Image"
width={800}
height={600}
/>3. Reserve Space for Ads/Embeds
// Reserve space for ad
<div style={{ minHeight: '250px' }}>
<AdComponent />
</div>
// Reserve space for YouTube
<div style={{ aspectRatio: '16 / 9' }}>
<YouTubeEmbed videoId="..." />
</div>4. Avoid Inserting Content Above Existing Content
// ❌ BAD: Inserts banner, shifts content down
function App() {
const [showBanner, setShowBanner] = useState(false);
useEffect(() => {
setTimeout(() => setShowBanner(true), 2000);
}, []);
return (
<>
{showBanner && <Banner />} {/* Shifts content */}
<Content />
</>
);
}
// ✅ GOOD: Overlay doesn't shift content
function App() {
return (
<>
<Banner style={{ position: 'fixed', top: 0 }} />
<Content style={{ marginTop: '60px' }} />
</>
);
}5. Use CSS Transform
/* ❌ BAD: Causes layout shift */
.box {
transition: width 0.3s;
}
.box:hover {
width: 200px;
}
/* ✅ GOOD: No layout shift */
.box {
transition: transform 0.3s;
}
.box:hover {
transform: scaleX(1.2);
}6. Preload Fonts
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
/>@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: optional; /* Or swap with size-adjust */
}7. Use size-adjust
/* Fallback font matches custom font size */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 105%; /* Adjust to match Inter */
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}Measuring Core Web Vitals
1. Chrome DevTools
Lighthouse:
1. Open Chrome DevTools
2. Go to Lighthouse tab
3. Select "Performance"
4. Click "Analyze page load"
5. View Core Web Vitals in reportPerformance Panel:
1. Open Chrome DevTools
2. Go to Performance tab
3. Record page load
4. Look for Web Vitals markers2. Web Vitals Library
npm install web-vitalsimport { onLCP, onINP, onCLS } from 'web-vitals';
// Measure LCP
onLCP((metric) => {
console.log('LCP:', metric.value);
sendToAnalytics(metric);
});
// Measure INP
onINP((metric) => {
console.log('INP:', metric.value);
sendToAnalytics(metric);
});
// Measure CLS
onCLS((metric) => {
console.log('CLS:', metric.value);
sendToAnalytics(metric);
});
// Send to analytics
function sendToAnalytics(metric: any) {
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,
});
}3. Next.js Integration
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<SpeedInsights />
</body>
</html>
);
}4. Custom Reporting
// lib/web-vitals.ts
export function reportWebVitals(metric: any) {
switch (metric.name) {
case 'FCP':
console.log('First Contentful Paint:', metric.value);
break;
case 'LCP':
console.log('Largest Contentful Paint:', metric.value);
break;
case 'CLS':
console.log('Cumulative Layout Shift:', metric.value);
break;
case 'INP':
console.log('Interaction to Next Paint:', metric.value);
break;
case 'TTFB':
console.log('Time to First Byte:', metric.value);
break;
}
// Send to your analytics
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify(metric),
headers: { 'Content-Type': 'application/json' },
});
}5. Real User Monitoring (RUM)
Google Analytics:
- Automatically collects Core Web Vitals
- Shows in Google Search Console
- Real user data, not synthetic
Third-Party Tools:
- New Relic
- Datadog
- Sentry
- Custom RUM solutions
6. PageSpeed Insights
1. Go to https://pagespeed.web.dev/
2. Enter your URL
3. View Core Web Vitals scores
4. Get optimization suggestions7. Google Search Console
1. Go to Google Search Console
2. Navigate to "Core Web Vitals" report
3. View field data (real users)
4. See which pages need improvement8. Web Vitals Extension
Install: Chrome Web Store → "Web Vitals"
Shows real-time CLS, LCP, INPCore Web Vitals Targets
Summary Table
| 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 |
Percentile Targets
Google uses the 75th percentile (p75) for Core Web Vitals:
- 75% of users should experience "Good" scores
- 25% of users can have "Needs Improvement" or "Poor"
- This accounts for varying network conditions and devices
Field Data vs Lab Data
Field Data (Real Users):
- Real user measurements
- Various devices/networks
- Used for SEO rankings
- More accurate for business impact
Lab Data (Synthetic):
- Controlled environment
- Consistent conditions
- Good for development
- May not reflect real users
Best Practice: Optimize based on field data from real users.
Quick Reference Checklists
LCP Checklist
- Identify LCP element
- Optimize LCP resource (image, text, video)
- Preload critical resources
- Reduce server response time (< 600ms TTFB)
- Minimize render-blocking resources
- Use CDN for static assets
- Optimize images (format, size, priority)
- Use modern image formats (WebP, AVIF)
INP Checklist
- Reduce JavaScript bundle size
- Code split heavy components
- Optimize event handlers
- Avoid long tasks (> 50ms)
- Use Web Workers for heavy computation
- Minimize third-party scripts
- Defer non-critical JavaScript
- Debounce/throttle input handlers
- Optimize main thread usage
CLS Checklist
- Add width/height to images
- Use font-display: swap for fonts
- Reserve space for ads/embeds
- Avoid inserting content above existing content
- Use skeleton screens for dynamic content
- Prefer transform/opacity for animations
- Use size-adjust for fonts
- Test layout stability
Best Practices Summary
LCP Best Practices
- Preload hero image
- Use CDN
- Optimize image size
- Eliminate render-blocking resources
- Fast server response (< 600ms)
- Use modern image formats
INP Best Practices
- Break up long tasks
- Use web workers
- Defer third-party scripts
- Code splitting
- Throttle/debounce handlers
- Optimize event handlers
CLS Best Practices
- Set image/video dimensions
- Reserve space for dynamic content
- Preload fonts with size-adjust
- Use transform instead of layout props
- Avoid inserting content above fold
Common Pitfalls
❌ No image dimensions: CLS
✅ Always set width/height
❌ Hero image lazy loaded: Slow LCP
✅ Use priority on LCP images
❌ Heavy JavaScript on main thread: Poor INP
✅ Break tasks, use web workers
❌ Fonts cause layout shift: Bad CLS
✅ Preload + size-adjust
❌ Third-parties in <head>: Everything slow
✅ Load after interactive
Common Questions
Q: Which metric is most important?
A: All three are important, but LCP often has the biggest impact on user perception and SEO. However, Google considers all three in rankings.
Q: Do I need perfect scores?
A: No. Aim for "Good" scores (75th percentile). Perfect scores are often impractical and may not provide significant benefit.
Q: How often should I measure?
A:
- During development: Use Lighthouse in DevTools
- In production: Monitor field data continuously
- After changes: Measure before/after optimizations
Q: Can I optimize all three at once?
A: Yes! Many optimizations help multiple metrics:
- Code splitting → Better LCP and INP
- Image optimization → Better LCP and CLS
- Reducing JavaScript → Better LCP and INP
Q: What if my scores are poor?
A:
- Identify the biggest issue (usually LCP)
- Focus on that first
- Measure impact of changes
- Iterate and improve
Next Steps
Learn more about specific optimization techniques:
- Interaction to Next Paint - Deep dive into INP optimization
- Layout Shift Prevention - Advanced CLS optimization
- Asset Optimization - Improve LCP with better assets
- Code Splitting Strategies - Reduce bundle size for better INP
Remember: Core Web Vitals measure real user experience. Focus on making your site fast, responsive, and stable for actual users, not just synthetic tests.