SEO Optimization11 min read

Core Web Vitals Optimization: INP, LCP, CLS Guide 2025

Master Core Web Vitals optimization in 2025 with this comprehensive guide covering INP (Interaction to Next Paint), LCP (Largest Contentful Paint), and CLS (Cumulative Layout Shift). Learn proven strategies, Next.js implementation examples, and performance monitoring techniques to achieve Google's targets and boost search rankings.

Digital Applied Team
October 20, 2025
11 min read

Key Takeaways

  • INP Replaced FID in March 2024: Interaction to Next Paint (INP) is now the responsiveness metric. Target ≤200ms by optimizing JavaScript execution and event handlers.
  • LCP Under 2.5 Seconds Is Critical: Largest Contentful Paint measures loading speed. Optimize images, reduce server response time, and eliminate render-blocking resources.
  • CLS Below 0.1 Prevents Layout Shifts: Cumulative Layout Shift ensures visual stability. Set image dimensions, preload fonts, and reserve space for dynamic content.
  • Next.js Built-In Optimizations Help: Use next/image for automatic optimization, priority loading, and proper sizing. Leverage server components for faster rendering.
  • Real User Monitoring Is Essential: 75th percentile of real-user data determines rankings. Monitor with Google Search Console and Web Vitals library continuously.
≤200ms

INP Target

≤2.5s

LCP Target

<0.1

CLS Target

75th

Ranking Factor

Understanding Core Web Vitals 2025

Core Web Vitals are Google's official metrics for measuring real user experience on the web. These metrics directly impact search rankings and are essential for competitive visibility in 2025.

The Three Core Web Vitals in 2025

INP - Interaction to Next Paint

Measures: Responsiveness to user interactions across the entire page lifecycle

Target: ≤200 milliseconds

What it captures: Time from user interaction (click, tap, keyboard input) to visual feedback

Why it matters: Replaces FID to measure all interactions, not just the first one

LCP - Largest Contentful Paint

Measures: Loading performance and perceived speed

Target: ≤2.5 seconds

What it captures: Time until the largest image or text block renders in the viewport

Why it matters: Indicates when main content becomes visible to users

CLS - Cumulative Layout Shift

Measures: Visual stability during page load

Target: <0.1

What it captures: Sum of all unexpected layout shift scores

Why it matters: Prevents frustrating content jumps that cause misclicks

How Google Measures Core Web Vitals

Google uses the 75th percentile of real-user data from the Chrome User Experience Report (CrUX). This means at least 75% of page visits must meet the "good" threshold for each metric to be classified as passing.

MetricGoodNeeds ImprovementPoor
INP≤200ms200-500ms>500ms
LCP≤2.5s2.5-4.0s>4.0s
CLS<0.10.1-0.25>0.25

INP - Interaction to Next Paint Optimization

INP measures how quickly your page responds to user interactions throughout the entire page lifecycle. Unlike its predecessor FID, which only measured the first interaction, INP considers all interactions and reports the worst one.

Understanding INP Components

INP Breakdown
  • Input Delay: Time from user action to event handler start (blocked by main thread tasks)
  • Processing Time: Time to execute event handlers and callbacks
  • Presentation Delay: Time to render and paint the visual update

INP Optimization Strategies

1. Break Up Long Tasks

Long JavaScript tasks (> 50ms) block the main thread and delay interactions. Split them into smaller chunks.

// ❌ Bad: Long synchronous task blocks main thread
function processLargeDataset(data) {
  const results = [];
  for (let i = 0; i < data.length; i++) {
    results.push(expensiveOperation(data[i]));
  }
  return results;
}

// ✅ Good: Break into chunks with scheduler.yield()
async function processLargeDataset(data) {
  const results = [];
  for (let i = 0; i < data.length; i++) {
    results.push(expensiveOperation(data[i]));

    // Yield to browser after each item
    if (i % 10 === 0) {
      await scheduler.yield();
    }
  }
  return results;
}

// ✅ Better: Use Web Workers for heavy computation
const worker = new Worker('/worker.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (e) => {
  console.log('Results:', e.data);
};

2. Optimize Event Handlers

Minimize work in event handlers, especially for high-frequency events like scroll and resize.

// ❌ Bad: Heavy work on every scroll
window.addEventListener('scroll', () => {
  const elements = document.querySelectorAll('.item');
  elements.forEach(el => {
    el.style.transform = `translateY(${window.scrollY}px)`;
  });
});

// ✅ Good: Debounce and use RAF
let ticking = false;
window.addEventListener('scroll', () => {
  if (!ticking) {
    window.requestAnimationFrame(() => {
      updateElements();
      ticking = false;
    });
    ticking = true;
  }
});

function updateElements() {
  const scrollY = window.scrollY;
  const elements = document.querySelectorAll('.item');
  elements.forEach(el => {
    el.style.transform = `translateY(${scrollY}px)`;
  });
}

3. Reduce JavaScript Execution Time

JavaScript Optimization Checklist
  • Code splitting: Load only necessary JavaScript per page
  • Tree shaking: Remove unused code from bundles
  • Lazy loading: Defer non-critical scripts and components
  • Minimize third-party scripts: Audit and remove unnecessary libraries
  • Use efficient APIs: Prefer modern, optimized browser APIs

4. Optimize DOM Updates

// ❌ Bad: Multiple reflows (layout thrashing)
elements.forEach(el => {
  const height = el.offsetHeight; // Read
  el.style.height = height + 10 + 'px'; // Write
});

// ✅ Good: Batch reads and writes
const heights = elements.map(el => el.offsetHeight); // Read all
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px'; // Write all
});

// ✅ Best: Use CSS transforms (no reflow)
element.style.transform = 'translateY(10px)';

Next.js INP Optimizations

// app/components/InteractiveButton.tsx
'use client'

import { useState, useTransition } from 'react'

export function InteractiveButton() {
  const [isPending, startTransition] = useTransition()
  const [count, setCount] = useState(0)

  const handleClick = () => {
    // Mark non-urgent updates with startTransition
    startTransition(() => {
      setCount(c => c + 1)
      // Heavy computation here won't block input
    })
  }

  return (
    <button
      onClick={handleClick}
      disabled={isPending}
    >
      Count: {count} {isPending && '...'}
    </button>
  )
}

// Lazy load heavy components
import dynamic from 'next/dynamic'

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false // Skip SSR for client-only components
})

LCP - Largest Contentful Paint Optimization

LCP measures how quickly the main content loads on your page. It's typically the largest image or text block visible in the viewport. Achieving LCP under 2.5 seconds requires optimizing server response time, resource delivery, and rendering.

What Counts as LCP Element?

  • Images: <img> elements and <image> elements inside <svg>
  • Background images: CSS background-image on block-level elements
  • Video thumbnails: Poster images of <video> elements
  • Text blocks: Block-level elements containing text nodes

LCP Optimization Strategies

1. Optimize Server Response Time (TTFB)

Time to First Byte should be under 600ms. Slow TTFB makes good LCP nearly impossible.

TTFB Optimization Techniques
  • Use a CDN: Serve content from edge locations close to users
  • Enable server caching: Cache database queries and API responses
  • Optimize database queries: Add indexes, reduce joins, use connection pooling
  • Use edge rendering: Deploy to edge functions (Vercel Edge, Cloudflare Workers)
  • Upgrade hosting infrastructure: Use modern, fast servers with adequate resources

2. Optimize and Prioritize Images

Images are the most common LCP elements. Optimization is critical.

// Next.js Image Optimization for LCP
import Image from 'next/image'

export default function Hero() {
  return (
    <div className="hero">
      <Image
        src="/hero-image.jpg"
        alt="Hero image"
        width={1200}
        height={600}
        priority // ⚠️ CRITICAL: Preload LCP image
        quality={85} // Balance quality vs size
        placeholder="blur" // Show blur while loading
        blurDataURL="data:image/jpeg;base64,..." // Blur preview
      />
    </div>
  )
}

// Responsive images with art direction
<Image
  src="/hero-desktop.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority
  sizes="(max-width: 768px) 100vw, 1200px"
/>
Image Optimization Checklist
  • Use WebP/AVIF: Modern formats are 30-50% smaller than JPEG
  • Add priority prop: Use on above-the-fold images (Next.js automatically preloads)
  • Proper dimensions: Always set width and height to prevent CLS
  • Compress images: Use tools like Squoosh, ImageOptim, or Sharp
  • Responsive images: Serve appropriately sized images for each viewport
  • Lazy load others: Only preload LCP image, lazy load below-fold images

3. Eliminate Render-Blocking Resources

// next.config.js - Optimize CSS and JavaScript
module.exports = {
  // Enable SWC minification (faster than Terser)
  swcMinify: true,

  // Optimize CSS
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },

  // Experimental features
  experimental: {
    optimizeCss: true, // CSS optimization
    optimizePackageImports: ['@/components'], // Tree-shake imports
  }
}

// app/layout.tsx - Optimize fonts
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Show fallback immediately
  preload: true, // Preload font files
  variable: '--font-inter'
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}

4. Use Resource Hints

// app/layout.tsx - Preconnect to external domains
export const metadata = {
  other: {
    'link': [
      // Preconnect to external domains
      { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
      { rel: 'preconnect', href: 'https://cdn.example.com', crossorigin: 'anonymous' },
      // DNS prefetch for lower priority origins
      { rel: 'dns-prefetch', href: 'https://analytics.google.com' }
    ]
  }
}

// Manual preload for critical resources
<link
  rel="preload"
  href="/critical.woff2"
  as="font"
  type="font/woff2"
  crossOrigin="anonymous"
/>

Server Components for Faster LCP

// Server Component - No client JS needed
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }) {
  // Fetch on server (faster, no client waterfall)
  const post = await getPost(params.slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

// Static generation for instant LCP
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

CLS - Cumulative Layout Shift Optimization

CLS measures visual stability by tracking unexpected layout shifts during the entire page lifecycle. A score below 0.1 means users won't experience frustrating content jumps that cause misclicks.

Common CLS Causes and Fixes

1. Images Without Dimensions

The most common cause of CLS. Always set explicit dimensions.

// ❌ Bad: No dimensions causes CLS
<img src="/image.jpg" alt="Product" />

// ✅ Good: Next.js Image with dimensions
<Image
  src="/image.jpg"
  alt="Product"
  width={800}
  height={600}
  // Next.js automatically reserves space
/>

// ✅ Good: Regular img with dimensions
<img
  src="/image.jpg"
  alt="Product"
  width="800"
  height="600"
  loading="lazy"
/>

// ✅ Good: CSS aspect ratio
<div style={{ aspectRatio: '16/9' }}>
  <img src="/image.jpg" alt="Product" />
</div>

2. Web Fonts Causing FOIT/FOUT

Font loading can cause text to shift. Use proper font loading strategies.

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

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Prevents invisible text
  variable: '--font-inter',
  preload: true,
  fallback: ['system-ui', 'arial']
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
  preload: true
})

// CSS with font-display
@font-face {
  font-family: 'Custom Font';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap; /* Show fallback immediately */
  font-weight: 400;
}

// Preload critical fonts
<link
  rel="preload"
  href="/fonts/custom.woff2"
  as="font"
  type="font/woff2"
  crossOrigin="anonymous"
/>

3. Dynamic Content Injection

Ads, embeds, and dynamic content often cause CLS. Reserve space.

// ❌ Bad: Ad injected without reserved space
<div id="ad-slot"></div>
<script>
  loadAd('ad-slot')
</script>

// ✅ Good: Reserved space with min-height
<div
  id="ad-slot"
  style={{
    minHeight: '250px',
    width: '300px',
    backgroundColor: '#f3f4f6'
  }}
>
  <AdComponent />
</div>

// ✅ Good: Skeleton placeholder
function AdPlaceholder() {
  return (
    <div className="w-[300px] h-[250px] bg-gray-100 animate-pulse">
      <div className="flex items-center justify-center h-full">
        <p className="text-gray-400">Advertisement</p>
      </div>
    </div>
  )
}

// Use with Suspense
<Suspense fallback={<AdPlaceholder />}>
  <AdComponent />
</Suspense>

4. CSS Animations and Transforms

Use transform and opacity for animations to avoid layout shifts.

// ❌ Bad: Animating layout properties causes CLS
.element {
  transition: height 0.3s, width 0.3s;
}
.element:hover {
  height: 200px;
  width: 300px;
}

// ✅ Good: Transform doesn't cause reflow
.element {
  transition: transform 0.3s, opacity 0.3s;
}
.element:hover {
  transform: scale(1.1);
  opacity: 0.9;
}

// ✅ Good: Content-visibility for off-screen content
.off-screen-section {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px; // Reserve space
}
CLS Prevention Checklist
  • Set image dimensions: Always include width and height attributes
  • Reserve ad space: Define min-height for ad slots and dynamic content
  • Preload fonts: Use font-display: swap and preload critical fonts
  • Avoid inserting content: Don't inject content above existing content
  • Use transform for animations: Prefer transform and opacity over layout properties
  • CSS aspect-ratio: Use for responsive containers with unknown dimensions

Next.js Optimization Strategies

Next.js 15 provides powerful built-in optimizations for Core Web Vitals. Leverage these features for maximum performance. Our web development services can help you implement these strategies effectively.

1. Server Components by Default

// Server Component (default) - Zero client JS
// app/components/BlogPost.tsx
export default async function BlogPost({ id }) {
  const post = await fetchPost(id)

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

// Client Component only when needed
// app/components/InteractiveButton.tsx
'use client'

export default function InteractiveButton() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

2. Image Optimization with next/image

// Automatic optimization, lazy loading, and modern formats
import Image from 'next/image'

// LCP image - use priority
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  priority // Preload immediately
  quality={90}
/>

// Below-fold images - lazy load automatically
<Image
  src="/thumbnail.jpg"
  alt="Thumbnail"
  width={400}
  height={300}
  // No priority = automatic lazy loading
/>

// Configure global image optimization
// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'], // Modern formats
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 31536000 // 1 year cache
  }
}

3. Static and Dynamic Rendering

// Static Generation (fastest)
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ id: post.id }))
}

export default async function Page({ params }) {
  const post = await getPost(params.id)
  return <BlogPost post={post} />
}

// Incremental Static Regeneration
export const revalidate = 3600 // Revalidate every hour

// Partial Prerendering (experimental)
export const experimental_ppr = true

// Dynamic segments only re-fetch when needed
export default async function Page() {
  const staticData = await getStaticData() // Cached

  return (
    <div>
      <StaticSection data={staticData} />
      <Suspense fallback={<Skeleton />}>
        <DynamicSection /> {/* Streamed */}
      </Suspense>
    </div>
  )
}

4. Code Splitting and Lazy Loading

import dynamic from 'next/dynamic'

// Lazy load heavy components
const HeavyChart = dynamic(() => import('@/components/Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false // Skip SSR for client-only components
})

// Lazy load with multiple exports
const Modal = dynamic(() =>
  import('@/components/Modal').then(mod => mod.Modal)
)

// Route-level code splitting (automatic)
// Each route gets its own bundle

// Manual code splitting for large libraries
import('lodash').then(({ debounce }) => {
  // Use debounce only when needed
})

5. Streaming and Suspense

// app/dashboard/page.tsx
import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <div>
      {/* Static content renders immediately */}
      <Header />

      {/* Stream slow components */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <Analytics />
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
    </div>
  )
}

// Analytics component fetches async
async function Analytics() {
  const data = await fetchAnalytics() // Slow query
  return <AnalyticsDisplay data={data} />
}

Performance Monitoring Setup

Continuous monitoring is essential to maintain good Core Web Vitals scores. Set up both real-user monitoring (RUM) and lab testing.

1. Web Vitals Library Integration

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  )
}

// Custom Web Vitals reporting
// app/web-vitals.tsx
'use client'

import { useReportWebVitals } from 'next/web-vitals'

export function WebVitals() {
  useReportWebVitals((metric) => {
    // Send to analytics service
    if (process.env.NODE_ENV === 'production') {
      const body = JSON.stringify({
        name: metric.name,
        value: metric.value,
        rating: metric.rating,
        delta: metric.delta,
        id: metric.id,
        navigationType: metric.navigationType
      })

      const url = '/api/analytics'

      // Use sendBeacon for reliability
      if (navigator.sendBeacon) {
        navigator.sendBeacon(url, body)
      } else {
        fetch(url, { method: 'POST', body, keepalive: true })
      }
    }
  })

  return null
}

// app/layout.tsx
import { WebVitals } from './web-vitals'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <WebVitals />
      </body>
    </html>
  )
}

2. Google Search Console

Core Web Vitals Report

Access: Google Search Console > Experience > Core Web Vitals

Data source: Chrome User Experience Report (CrUX) - real user data

Shows: URLs grouped by status (Good, Needs Improvement, Poor)

Threshold: 75th percentile of page visits over 28 days

3. PageSpeed Insights

Use PageSpeed Insights for both field (real-user) and lab (simulated) data:

  • Field Data: Real user measurements from CrUX (past 28 days)
  • Lab Data: Simulated performance in controlled environment
  • Opportunities: Specific recommendations to improve metrics
  • Diagnostics: Additional information about page performance

4. Lighthouse CI

// .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Run Lighthouse CI
        run: |
          npm install -g @lhci/cli
          lhci autorun

      - name: Upload results
        uses: actions/upload-artifact@v3
        with:
          name: lighthouse-results
          path: .lighthouseci

// lighthouserc.json
{
  "ci": {
    "collect": {
      "startServerCommand": "npm run start",
      "url": ["http://localhost:3000/"],
      "numberOfRuns": 3
    },
    "assert": {
      "preset": "lighthouse:recommended",
      "assertions": {
        "largest-contentful-paint": ["error", {"maxNumericValue": 2500}],
        "cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}],
        "total-blocking-time": ["error", {"maxNumericValue": 200}]
      }
    },
    "upload": {
      "target": "temporary-public-storage"
    }
  }
}

5. Chrome DevTools Performance Panel

Local Performance Testing
  • Record: Capture page load and interactions
  • Throttling: Simulate slow 3G, 4G network conditions
  • CPU throttling: Test on slower devices (4x slowdown)
  • Web Vitals overlay: Real-time metrics during testing
  • Long Tasks: Identify JavaScript blocking main thread

Common Issues & Solutions

Issue 1: Poor INP on Mobile

Symptoms

Desktop INP is good, but mobile shows 300-500ms. Buttons feel slow and unresponsive on phones.

Solution
  • Reduce JavaScript bundle size (mobile has slower CPUs)
  • Use React.memo() to prevent unnecessary re-renders
  • Implement virtual scrolling for long lists (react-window)
  • Remove heavy third-party scripts on mobile
  • Use CSS-only solutions instead of JavaScript when possible

Issue 2: LCP Over 4 Seconds

Symptoms

Hero image takes forever to load. PageSpeed Insights shows high TTFB and render delays.

Solution
  • Add priority prop to LCP image in Next.js
  • Reduce image size (use WebP/AVIF, compress to 85% quality)
  • Use CDN for faster delivery (Vercel, Cloudflare)
  • Enable HTTP/2 or HTTP/3 for multiplexing
  • Optimize server response time (upgrade hosting, cache)
  • Remove render-blocking CSS/JS above LCP element

Issue 3: CLS from Ads and Embeds

Symptoms

Content jumps when ads load. YouTube embeds cause layout shifts. CLS score of 0.25+.

Solution
  • Reserve space with min-height matching ad dimensions
  • Use aspect-ratio CSS for responsive embeds (16:9 for videos)
  • Load ads below-the-fold with lazy loading
  • Use skeleton placeholders while content loads
  • Implement Suspense boundaries with proper fallbacks

Issue 4: Third-Party Scripts Destroying Performance

Symptoms

Analytics, chat widgets, and tracking scripts slow everything. Long tasks block main thread.

Solution

Use Next.js Script component with proper strategies:

import Script from 'next/script'

// After page interactive
<Script
  src="https://analytics.example.com/script.js"
  strategy="afterInteractive"
/>

// Lazy load non-critical scripts
<Script
  src="https://widget.example.com/chat.js"
  strategy="lazyOnload"
/>

// Worker strategy (experimental)
<Script
  src="https://heavy-script.js"
  strategy="worker"
/>
Frequently Asked Questions

Ready to Optimize Your Core Web Vitals?

Core Web Vitals optimization is critical for search visibility, user experience, and conversions in 2025. Digital Applied specializes in web development and SEO optimization for Next.js and React applications.

Free site audit • Performance roadmap • Guaranteed improvements