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.
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.
INP Target
LCP Target
CLS Target
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
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
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
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.
| Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
| INP | ≤200ms | 200-500ms | >500ms |
| LCP | ≤2.5s | 2.5-4.0s | >4.0s |
| CLS | <0.1 | 0.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
- 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
- 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.
- 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"
/>- 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
}- 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
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
- 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
Desktop INP is good, but mobile shows 300-500ms. Buttons feel slow and unresponsive on phones.
- 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
Hero image takes forever to load. PageSpeed Insights shows high TTFB and render delays.
- 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
Content jumps when ads load. YouTube embeds cause layout shifts. CLS score of 0.25+.
- 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
Analytics, chat widgets, and tracking scripts slow everything. Long tasks block main thread.
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"
/>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
Related Articles
Link Building Strategies 2025: Authority Backlinks
Master link building in 2025: digital PR, HARO optimization, resource pages, broken link building. Complete guide with outreach templates and proven strategies.
Schema Markup Guide: Structured Data for Rich Results 2025
Implement schema markup for rich results: JSON-LD, Article, Product, HowTo schemas. Complete guide with code examples and validation.
Technical SEO Checklist 2025: Complete Implementation Guide
Master technical SEO with our complete 2025 checklist: Core Web Vitals, structured data, mobile-first, crawlability. Step-by-step implementation guide.