osmoto.
Case StudiesBlogBook Consultation

Services

Stripe IntegrationSubscription BillingPayment Automation & AINext.js OptimizationAudit & Fix

Solutions

For FoundersFor SaaS CompaniesFor E-Commerce StoresFor Marketplaces

Resources

Implementation GuideWebhook Best PracticesPCI Compliance GuideStripe vs Alternatives
Case StudiesBlog
Book Consultation
osmoto.

Professional Stripe integration services

Services

  • Stripe Integration
  • Subscription Billing
  • E-Commerce Integration
  • Next.js Optimization
  • Audit & Fix

Solutions

  • For Founders
  • For SaaS
  • For E-Commerce
  • For Marketplaces
  • Integration as a Service

Resources

  • Implementation Guide
  • Webhook Guide
  • PCI Compliance
  • Stripe vs Alternatives

Company

  • About
  • Case Studies
  • Process
  • Pricing
  • Contact
© 2026 Osmoto · Professional Stripe Integration Services
Back to Blog
Performance13 min read

Fixing Next.js Core Web Vitals: LCP, FID, and CLS Issues

Your Next.js application scores perfectly in Lighthouse during development, but production metrics tell a different story. Real users report slow loading times,...

Osmoto Team

Senior Software Engineer

January 15, 2026
Fixing Next.js Core Web Vitals: LCP, FID, and CLS Issues

Your Next.js application scores perfectly in Lighthouse during development, but production metrics tell a different story. Real users report slow loading times, janky interactions, and layout shifts that make your site feel broken. The culprit? Core Web Vitals issues that only surface under real-world conditions with actual network latency, device constraints, and user behavior patterns.

Core Web Vitals directly impact both user experience and search rankings. Google uses these metrics as ranking factors, and more importantly, poor scores correlate with higher bounce rates and lower conversion rates. A 100ms improvement in Largest Contentful Paint (LCP) can increase conversion rates by up to 8%, while reducing Cumulative Layout Shift (CLS) eliminates the frustrating experience of users accidentally clicking wrong elements due to unexpected page shifts.

This guide provides a systematic approach to diagnosing and fixing the three Core Web Vitals in Next.js applications: LCP (loading performance), First Input Delay/Interaction to Next Paint (interactivity), and CLS (visual stability). We'll cover both App Router and Pages Router implementations, focusing on production-ready solutions that work at scale.

Understanding Core Web Vitals in Next.js Context

Largest Contentful Paint (LCP) - Loading Performance

LCP measures when the largest content element becomes visible to users. In Next.js applications, this is typically an image, video, or large text block. The target is under 2.5 seconds, but modern users expect sub-second loading.

Common LCP elements in Next.js apps:

  • Hero images loaded with next/image
  • Large text blocks from CMS content
  • Video elements or embedded media
  • Product images in e-commerce applications

Next.js provides several built-in optimizations for LCP, but they require proper configuration. The framework's automatic code splitting can actually hurt LCP if critical resources are split into separate chunks that load sequentially rather than in parallel.

First Input Delay (FID) and Interaction to Next Paint (INP)

Google is transitioning from FID to INP as the interactivity metric. FID measures the delay between user interaction and browser response, while INP measures the time from interaction to visual update. Next.js applications often struggle with INP due to:

  • Large JavaScript bundles blocking the main thread
  • Hydration delays in server-side rendered components
  • Expensive re-renders triggered by user interactions
  • Third-party scripts interfering with main thread execution

Cumulative Layout Shift (CLS) - Visual Stability

CLS quantifies unexpected layout shifts during page loading. Next.js applications commonly experience CLS from:

  • Images loading without proper dimensions
  • Fonts swapping after initial render
  • Dynamic content injection (ads, social widgets)
  • CSS-in-JS libraries causing style recalculation

The target CLS score is under 0.1, with 0.0 being ideal for critical user flows like checkout processes.

Diagnosing Core Web Vitals Issues

Setting Up Proper Measurement

Real User Monitoring (RUM) provides more accurate data than synthetic testing. Implement Web Vitals measurement in your Next.js app:

// lib/web-vitals.ts import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'; function sendToAnalytics(metric: any) { // Send to your analytics service if (typeof window !== 'undefined') { // Example: Google Analytics 4 gtag('event', metric.name, { event_category: 'Web Vitals', value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value), event_label: metric.id, non_interaction: true, }); } } export function reportWebVitals() { getCLS(sendToAnalytics); getFID(sendToAnalytics); getFCP(sendToAnalytics); getLCP(sendToAnalytics); getTTFB(sendToAnalytics); }

For App Router, add to your root layout:

// app/layout.tsx 'use client'; import { useEffect } from 'react'; import { reportWebVitals } from '../lib/web-vitals'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { useEffect(() => { reportWebVitals(); }, []); return ( <html lang="en"> <body>{children}</body> </html> ); }

Identifying LCP Elements

Use the Web Vitals Chrome extension or browser DevTools to identify your LCP element. Common issues and their indicators:

Image-based LCP problems:

  • LCP element is an unoptimized image
  • Image loads after other resources complete
  • Missing priority prop on above-the-fold images

Text-based LCP problems:

  • Large text blocks using web fonts
  • Font swap causing layout recalculation
  • Text content loaded via client-side JavaScript

Resource loading issues:

  • LCP element depends on JavaScript execution
  • Critical resources blocked by non-critical ones
  • Missing preload hints for essential assets

Fixing LCP Issues

Optimizing Images for LCP

The next/image component provides automatic optimization, but requires proper configuration for LCP improvements:

// components/HeroImage.tsx import Image from 'next/image'; export function HeroImage() { return ( <Image src="/hero-image.jpg" alt="Hero image" width={1200} height={600} priority // Critical for above-the-fold images placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..." // Inline blur sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" style={{ width: '100%', height: 'auto', }} /> ); }

Key optimizations:

  • priority={true} prevents lazy loading for LCP images
  • Proper sizes attribute ensures correct image variant selection
  • placeholder="blur" provides immediate visual feedback
  • Explicit width/height prevents layout shift

Preloading Critical Resources

Use Next.js built-in preloading for critical assets:

// app/head.tsx or pages/_document.tsx export default function Head() { return ( <> <link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossOrigin="anonymous" /> <link rel="preload" href="/hero-image.jpg" as="image" /> </> ); }

For dynamic imports that affect LCP, consider using next/dynamic with preloading:

// components/CriticalComponent.tsx import dynamic from 'next/dynamic'; const CriticalComponent = dynamic(() => import('./HeavyComponent'), { loading: () => <div>Loading...</div>, ssr: true, // Ensure server-side rendering for LCP content });

Font Optimization for LCP

Font loading significantly impacts LCP when text is the largest contentful element. Configure optimal font loading:

/* globals.css */ @font-face { font-family: 'Inter'; font-style: normal; font-weight: 100 900; font-display: swap; /* Prevents invisible text during font load */ src: url('/fonts/inter-var.woff2') format('woff2'); }

Use the next/font optimization for Google Fonts:

// app/layout.tsx import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', preload: true, }); export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" className={inter.className}> <body>{children}</body> </html> ); }

Fixing Interactivity Issues (FID/INP)

Reducing JavaScript Bundle Size

Large JavaScript bundles block the main thread, causing poor FID/INP scores. Analyze your bundle:

# Install bundle analyzer npm install --save-dev @next/bundle-analyzer # Add to next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); module.exports = withBundleAnalyzer({ // your config }); # Generate bundle analysis ANALYZE=true npm run build

Common optimization strategies:

Code splitting at component level:

// Instead of importing everything upfront import { HeavyChart, ExpensiveModal, RarelyUsedFeature } from './components'; // Split by usage patterns const HeavyChart = dynamic(() => import('./HeavyChart')); const ExpensiveModal = dynamic(() => import('./ExpensiveModal'));

Tree shaking third-party libraries:

// Bad: imports entire library import _ from 'lodash'; // Good: import only needed functions import { debounce, throttle } from 'lodash'; // Better: use specific imports import debounce from 'lodash/debounce';

Optimizing Hydration Performance

Hydration delays directly impact INP. Use selective hydration for better performance:

// components/InteractiveSection.tsx import { useState, useEffect } from 'react'; import dynamic from 'next/dynamic'; // Only hydrate interactive components when needed const InteractiveWidget = dynamic(() => import('./InteractiveWidget'), { ssr: false, // Skip SSR for client-only interactivity }); export function InteractiveSection() { const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []); if (!isClient) { return <div>Loading interactive content...</div>; } return <InteractiveWidget />; }

For App Router, use the use client directive strategically:

// app/dashboard/page.tsx - Server Component by default import { Suspense } from 'react'; import StaticContent from './StaticContent'; import InteractiveChart from './InteractiveChart'; export default function DashboardPage() { return ( <div> <StaticContent /> {/* Rendered on server */} <Suspense fallback={<div>Loading chart...</div>}> <InteractiveChart /> {/* Client component with 'use client' */} </Suspense> </div> ); }

Third-Party Script Optimization

Third-party scripts often cause INP issues. Use Next.js Script component for optimal loading:

// app/layout.tsx import Script from 'next/script'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body> {children} <Script src="https://www.googletagmanager.com/gtag/js?id=GA_TRACKING_ID" strategy="afterInteractive" // Load after page is interactive /> <Script id="google-analytics"> {` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'GA_TRACKING_ID'); `} </Script> </body> </html> ); }

Script loading strategies:

  • beforeInteractive: Critical scripts (rare)
  • afterInteractive: Analytics, social widgets
  • lazyOnload: Non-critical scripts
  • worker: Experimental Partytown integration

Fixing Cumulative Layout Shift (CLS)

Preventing Image Layout Shifts

Always specify image dimensions to prevent layout shifts:

// Bad: No dimensions specified <Image src="/product.jpg" alt="Product" /> // Good: Explicit dimensions <Image src="/product.jpg" alt="Product" width={400} height={300} style={{ width: '100%', height: 'auto', }} /> // Better: Responsive with aspect ratio <div style={{ aspectRatio: '4/3', position: 'relative' }}> <Image src="/product.jpg" alt="Product" fill style={{ objectFit: 'cover' }} /> </div>

Font Loading Without Layout Shift

Prevent font swap layout shifts using size-adjust:

/* globals.css */ @font-face { font-family: 'CustomFont'; src: url('/fonts/custom-font.woff2') format('woff2'); font-display: swap; size-adjust: 100.06%; /* Adjust to match fallback font metrics */ } /* Fallback font with similar metrics */ .font-loading { font-family: 'CustomFont', Arial, sans-serif; }

Use the next/font automatic size adjustment:

// app/layout.tsx import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], adjustFontFallback: true, // Automatically adjusts fallback metrics });

Dynamic Content Without Layout Shift

Reserve space for dynamic content to prevent shifts:

// components/DynamicContent.tsx import { useState, useEffect } from 'react'; export function DynamicContent() { const [content, setContent] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { fetchContent().then((data) => { setContent(data); setIsLoading(false); }); }, []); // Reserve space with skeleton loader if (isLoading) { return ( <div className="space-y-4"> <div className="h-6 bg-gray-200 rounded animate-pulse" /> <div className="h-4 bg-gray-200 rounded animate-pulse w-3/4" /> <div className="h-4 bg-gray-200 rounded animate-pulse w-1/2" /> </div> ); } return <div>{content}</div>; }

Advanced Optimization Techniques

Server Components for Better Performance

App Router's Server Components reduce JavaScript bundle size and improve all Core Web Vitals:

// app/products/page.tsx - Server Component import { getProducts } from '../lib/data'; import ProductCard from './ProductCard'; export default async function ProductsPage() { const products = await getProducts(); // Runs on server return ( <div className="grid grid-cols-3 gap-4"> {products.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </div> ); } // app/products/ProductCard.tsx - Server Component export default function ProductCard({ product }) { return ( <div className="border rounded p-4"> <h3>{product.name}</h3> <p>{product.description}</p> <AddToCartButton productId={product.id} /> {/* Client Component */} </div> ); }

Streaming for Progressive Loading

Use React 18 Suspense with streaming to improve perceived performance:

// app/dashboard/page.tsx import { Suspense } from 'react'; import QuickStats from './QuickStats'; import DetailedChart from './DetailedChart'; import RecentActivity from './RecentActivity'; export default function Dashboard() { return ( <div className="space-y-6"> <QuickStats /> {/* Fast-loading component */} <Suspense fallback={<ChartSkeleton />}> <DetailedChart /> {/* Slower component */} </Suspense> <Suspense fallback={<ActivitySkeleton />}> <RecentActivity /> {/* Another slow component */} </Suspense> </div> ); }

Critical CSS Inlining

For pages with custom styling that affects LCP, consider inlining critical CSS:

// next.config.js module.exports = { experimental: { optimizeCss: true, // Enables CSS optimization }, compiler: { removeConsole: process.env.NODE_ENV === 'production', }, };

Manual critical CSS extraction:

// lib/critical-css.ts export function getCriticalCSS(pageName: string) { // Extract critical CSS for specific pages const criticalStyles = { home: ` .hero { min-height: 60vh; } .hero-image { aspect-ratio: 16/9; } `, product: ` .product-grid { display: grid; grid-template-columns: 1fr 1fr; } .product-image { aspect-ratio: 1/1; } `, }; return criticalStyles[pageName] || ''; }

Common Pitfalls and Edge Cases

Hydration Mismatches Affecting CLS

Server-client content mismatches cause layout shifts during hydration:

// Bad: Different content on server vs client function UserGreeting() { const [user, setUser] = useState(null); useEffect(() => { setUser(getCurrentUser()); }, []); // This causes a layout shift when user loads return <div>{user ? `Hello, ${user.name}!` : 'Loading...'}</div>; } // Good: Consistent server/client rendering function UserGreeting() { const [user, setUser] = useState(null); const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); setUser(getCurrentUser()); }, []); // Reserve space to prevent layout shift return ( <div className="min-h-[2rem] flex items-center"> {isClient && user ? `Hello, ${user.name}!` : ''} </div> ); }

CSS-in-JS Performance Impact

CSS-in-JS libraries can hurt all three Core Web Vitals. If you must use them, optimize their usage:

// Bad: Runtime style generation const StyledButton = styled.button` background: ${props => props.primary ? 'blue' : 'gray'}; padding: ${props => props.size === 'large' ? '12px 24px' : '8px 16px'}; `; // Better: Pre-computed styles const buttonStyles = { base: 'px-4 py-2 rounded', primary: 'bg-blue-500 text-white', secondary: 'bg-gray-500 text-white', large: 'px-6 py-3', small: 'px-2 py-1', }; function Button({ primary, size, children }) { const classes = [ buttonStyles.base, primary ? buttonStyles.primary : buttonStyles.secondary, buttonStyles[size] || '', ].join(' '); return <button className={classes}>{children}</button>; }

Third-Party Widget Integration

Social media widgets, chat systems, and ads commonly cause Core Web Vitals issues:

// components/SocialWidget.tsx import { useEffect, useRef, useState } from 'react'; export function TwitterEmbed({ tweetId }: { tweetId: string }) { const containerRef = useRef<HTMLDivElement>(null); const [isVisible, setIsVisible] = useState(false); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } }, { threshold: 0.1 } ); if (containerRef.current) { observer.observe(containerRef.current); } return () => observer.disconnect(); }, []); return ( <div ref={containerRef} className="min-h-[200px] border rounded" // Reserve space > {isVisible && ( <blockquote className="twitter-tweet"> <a href={`https://twitter.com/user/status/${tweetId}`}> Loading tweet... </a> </blockquote> )} </div> ); }

Best Practices Summary

LCP Optimization Checklist

  • Add priority prop to above-the-fold images
  • Preload critical resources (fonts, hero images)
  • Optimize font loading with font-display: swap
  • Use Server Components to reduce JavaScript bundle size
  • Implement proper image sizing and responsive images
  • Minimize render-blocking resources

FID/INP Optimization Checklist

  • Analyze and reduce JavaScript bundle size
  • Use code splitting for non-critical components
  • Optimize third-party script loading
  • Implement selective hydration
  • Use Server Components where possible
  • Debounce expensive operations

CLS Optimization Checklist

  • Specify dimensions for all images and media
  • Reserve space for dynamic content
  • Optimize font loading to prevent swaps
  • Use skeleton loaders for loading states
  • Test with slow network conditions
  • Avoid inserting content above existing content

Monitoring and Maintenance

  • Implement Real User Monitoring (RUM)
  • Set up Core Web Vitals alerts
  • Regular performance audits
  • Test on various devices and networks
  • Monitor after deployments

Conclusion

Fixing Core Web Vitals in Next.js requires a systematic approach that addresses each metric's root causes. LCP improvements come from optimizing critical resource loading and reducing render-blocking JavaScript. FID/INP optimization focuses on reducing main thread work and improving interactivity. CLS fixes prevent unexpected layout shifts through proper space reservation and optimized loading strategies.

The key is measuring real user performance, not just synthetic lab scores. Implement proper monitoring, test optimizations under realistic conditions, and prioritize fixes based on actual user impact. Remember that Core Web Vitals optimization is an ongoing process—new features, content, and third-party integrations can introduce regressions.

If your Next.js application needs comprehensive performance optimization, including Core Web Vitals improvements, App Router migration, and advanced optimization techniques, our Next.js optimization service provides expert analysis and implementation. We've helped numerous applications achieve consistent Core Web Vitals scores in the "Good" range while maintaining feature richness and development velocity.

Related Articles

Building Fast Stripe Payment Pages: Next.js Core Web Vitals Optimization
Performance
Building Fast Stripe Payment Pages: Next.js Core Web Vitals Optimization
Every second of delay on your payment page costs you conversions. Research shows that a one-second delay in page load can reduce conversions by 7%, and for paym...

Need Expert Implementation?

I provide professional Stripe integration and Next.js optimization services with fixed pricing and fast delivery.