Google uses Core Web Vitals as a ranking factor. Sites that load fast, respond quickly, and maintain visual stability rank higher—and provide better user experiences.

This guide explains what Core Web Vitals are, why they matter, and how to optimize each metric for your website.

What Are Core Web Vitals?

Core Web Vitals are three metrics that Google uses to measure user experience:

| Metric | Measures | Good | Needs Improvement | Poor | |--------|----------|------|-------------------|------| | LCP | Loading performance | ≤2.5s | 2.5s - 4.0s | >4.0s | | INP | Interactivity | ≤200ms | 200ms - 500ms | >500ms | | CLS | Visual stability | ≤0.1 | 0.1 - 0.25 | >0.25 |

These metrics replaced the old "page speed" thinking with specific, measurable targets that correlate with real user experience.

LCP: Largest Contentful Paint

LCP measures how long it takes for the largest visible content element to render. This is usually:

  • A hero image
  • A large text block
  • A video thumbnail

What Causes Poor LCP

  1. Slow server response time - TTFB (Time to First Byte) too high
  2. Render-blocking resources - CSS and JavaScript blocking the page
  3. Slow resource load times - Large images, unoptimized fonts
  4. Client-side rendering - JavaScript building the page instead of server

How to Improve LCP

1. Optimize Images

Images are the most common LCP element. Optimize them aggressively:

// Next.js Image component - automatic optimization
import Image from 'next/image'

<Image
  src="/hero-image.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority  // Preload LCP image
  sizes="(max-width: 768px) 100vw, 1200px"
/>

Key optimizations:

  • Use modern formats (WebP, AVIF)
  • Serve appropriately sized images (srcset)
  • Lazy load below-the-fold images
  • Preload LCP images with priority prop

2. Preload Critical Resources

Tell the browser what to load first:

<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero-image.webp" as="image">

3. Reduce Server Response Time

  • Use a CDN (Vercel, Cloudflare, etc.)
  • Implement caching headers
  • Optimize database queries
  • Consider edge rendering

4. Eliminate Render-Blocking Resources

// Next.js Script component with strategy
import Script from 'next/script'

<Script
  src="https://example.com/script.js"
  strategy="afterInteractive"  // Load after page is interactive
/>

Script strategies:

  • beforeInteractive - Load before hydration (critical scripts)
  • afterInteractive - Load after page is interactive (analytics)
  • lazyOnload - Load during idle time (low priority)

5. Use Server-Side Rendering

Client-side rendering delays LCP because JavaScript must execute before content appears:

// Next.js App Router - Server Components by default
// This renders on the server, fast LCP
export default function Page() {
  return <h1>Hello World</h1>  // Rendered in HTML, not JS
}

Measuring LCP

// Using web-vitals library
import { onLCP } from 'web-vitals'

onLCP((metric) => {
  console.log('LCP:', metric.value)
  // Send to analytics
})

INP: Interaction to Next Paint

INP replaced FID (First Input Delay) in March 2024. It measures how quickly your site responds to user interactions—clicks, taps, and key presses.

What Causes Poor INP

  1. Long JavaScript tasks - Tasks blocking the main thread
  2. Heavy event handlers - Slow click/change handlers
  3. Large DOM size - Many elements to update
  4. Hydration delays - React/framework startup time

How to Improve INP

1. Break Up Long Tasks

Long tasks (>50ms) block user interaction:

// Bad: One long task
function processAllItems(items) {
  items.forEach(item => heavyProcess(item))
}

// Good: Yield to main thread
async function processAllItems(items) {
  for (const item of items) {
    heavyProcess(item)
    // Yield to allow interactions
    await new Promise(resolve => setTimeout(resolve, 0))
  }
}

2. Optimize Event Handlers

// Bad: Heavy computation in handler
button.addEventListener('click', () => {
  const result = expensiveCalculation()  // Blocks UI
  updateUI(result)
})

// Good: Defer non-critical work
button.addEventListener('click', () => {
  requestAnimationFrame(() => {
    const result = expensiveCalculation()
    updateUI(result)
  })
})

3. Reduce JavaScript Bundle Size

# Analyze your bundle
npm run build
npx @next/bundle-analyzer
  • Code split by route
  • Dynamic imports for heavy components
  • Remove unused dependencies
// Dynamic import - loads only when needed
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <Spinner />,
})

4. Optimize React Rendering

// Memoize expensive components
const ExpensiveList = memo(function ExpensiveList({ items }) {
  return items.map(item => <Item key={item.id} {...item} />)
})

// Use useCallback for stable references
const handleClick = useCallback(() => {
  doSomething()
}, [])

Measuring INP

import { onINP } from 'web-vitals'

onINP((metric) => {
  console.log('INP:', metric.value)
  console.log('Interaction target:', metric.attribution?.interactionTarget)
})

CLS: Cumulative Layout Shift

CLS measures visual stability—how much the page layout shifts unexpectedly while loading.

What Causes Poor CLS

  1. Images without dimensions - Browser doesn't know size until loaded
  2. Ads and embeds - Dynamic content injected late
  3. Web fonts - Text reflow when fonts load
  4. Dynamic content - Content inserted above existing content

How to Improve CLS

1. Always Set Image Dimensions

// Bad: No dimensions, causes layout shift
<img src="/photo.jpg" alt="Photo" />

// Good: Dimensions specified
<img src="/photo.jpg" alt="Photo" width={800} height={600} />

// Best: Next.js Image handles this automatically
<Image src="/photo.jpg" alt="Photo" width={800} height={600} />

2. Reserve Space for Dynamic Content

/* Reserve space for ad container */
.ad-container {
  min-height: 250px;
  width: 100%;
}

/* Aspect ratio box for videos */
.video-container {
  aspect-ratio: 16 / 9;
  width: 100%;
}

3. Optimize Font Loading

// Next.js font optimization
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // Show fallback immediately
})

Or use font-display in CSS:

@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap;  /* or 'optional' for less shift */
}

4. Avoid Inserting Content Above Existing Content

// Bad: New content pushes existing content down
const [notifications, setNotifications] = useState([])

// Notification appears at top, shifting everything
<div>
  {notifications.map(n => <Notification key={n.id} {...n} />)}
  <MainContent />
</div>

// Good: Reserve space or add at bottom
<div>
  <MainContent />
  {notifications.map(n => <Notification key={n.id} {...n} />)}
</div>

5. Use CSS Transform for Animations

/* Bad: Animating layout properties */
.element {
  animation: slide 0.3s;
}
@keyframes slide {
  from { margin-left: -100px; }
  to { margin-left: 0; }
}

/* Good: Animating transform (no layout shift) */
.element {
  animation: slide 0.3s;
}
@keyframes slide {
  from { transform: translateX(-100px); }
  to { transform: translateX(0); }
}

Measuring CLS

import { onCLS } from 'web-vitals'

onCLS((metric) => {
  console.log('CLS:', metric.value)
  // Identify which elements shifted
  metric.entries.forEach(entry => {
    console.log('Shifted element:', entry.sources)
  })
})

Tools for Measuring Core Web Vitals

Lab Tools (Synthetic Testing)

| Tool | Best For | |------|----------| | Lighthouse | Quick local audits | | PageSpeed Insights | Production URL analysis | | Chrome DevTools | Detailed debugging | | WebPageTest | Advanced analysis |

Field Data (Real User Monitoring)

| Tool | Best For | |------|----------| | Chrome UX Report (CrUX) | Real-world data from Chrome users | | Search Console | Core Web Vitals report | | Google Analytics 4 | Custom performance tracking |

In Your Code

// web-vitals library
import { onCLS, onLCP, onINP } from 'web-vitals'

function sendToAnalytics(metric) {
  // Send to GA4 or your analytics
  gtag('event', metric.name, {
    value: Math.round(metric.value),
    metric_id: metric.id,
    metric_value: metric.value,
    metric_delta: metric.delta,
  })
}

onCLS(sendToAnalytics)
onLCP(sendToAnalytics)
onINP(sendToAnalytics)

Next.js Performance Best Practices

Next.js provides built-in optimizations. Use them:

1. Image Optimization

import Image from 'next/image'

// Automatic: format conversion, sizing, lazy loading
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority  // For LCP images
/>

2. Font Optimization

import { Inter, Playfair_Display } from 'next/font/google'

const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' })

3. Script Optimization

import Script from 'next/script'

// Analytics - load after page is interactive
<Script
  src="https://www.googletagmanager.com/gtm.js"
  strategy="afterInteractive"
/>

4. Server Components (App Router)

// Server Component - no JavaScript sent to client
export default async function Page() {
  const data = await fetchData()
  return <div>{data}</div>
}

// Client Component - only when needed
'use client'
export default function InteractiveWidget() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

Performance vs SEO Connection

Performance directly impacts SEO:

  1. Ranking factor - Core Web Vitals are part of Google's page experience signals
  2. Crawl budget - Slow sites get crawled less frequently
  3. User engagement - Fast sites have lower bounce rates
  4. Conversion - Speed directly correlates with conversion rates

For more on the SEO connection, see our Technical SEO Fundamentals guide.

Monitoring in Production

Set up monitoring to catch regressions:

  1. Google Search Console - Core Web Vitals report
  2. Google Analytics 4 - Track with proper analytics setup
  3. Real User Monitoring - web-vitals library to your analytics

Quick Wins Checklist

Start with these high-impact, low-effort improvements:

  • [ ] Add priority to your LCP image
  • [ ] Set width/height on all images
  • [ ] Use Next.js Image component
  • [ ] Move analytics to afterInteractive
  • [ ] Reserve space for ads/embeds
  • [ ] Use font-display: swap
  • [ ] Enable Vercel/CDN caching

This post is part of the Web Optimization series. Performance is just one piece—combine with SEO, analytics, and structured data for maximum impact.