Core Web Vitals: How to Achieve 90+ in PageSpeed on Next.js
Practical guide to optimizing Core Web Vitals in Next.js. LCP, FID, CLS - how to achieve 90+ score in Google PageSpeed Insights.
Core Web Vitals are key metrics that impact Google rankings. In this guide, you'll learn how to optimize Next.js and achieve a score above 90 points in PageSpeed Insights.
What Are Core Web Vitals?
Google introduced Core Web Vitals as a ranking factor in 2021. These are three key metrics that measure different aspects of user experience. Largest Contentful Paint measures the loading time of the largest element on the page and should be less than 2.5 seconds. First Input Delay (replaced by Interaction to Next Paint) measures interaction responsiveness and should be below 100ms for FID or 200ms for INP. Cumulative Layout Shift measures visual layout stability and should be below 0.1. Each of these metrics significantly impacts the overall PageSpeed score.
Optimizing LCP in Next.js
1. Image Optimization with next/image
The next/image component automatically optimizes images but requires proper configuration:
import Image from 'next/image' // ✅ Correctly - with priority for hero image <Image src="/hero-image.jpg" alt="Hero image" width={1200} height={600} priority={true} placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..." /> // ❌ Wrong - without optimization <img src="/hero-image.jpg" alt="Hero" />
2. Preload Critical Resources
Preloading critical resources speeds up page loading:
// pages/_document.js import { Head, Html, Main, NextScript } from "next/document"; export default function Document() { return ( <Html> <Head> <link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossOrigin="" /> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="dns-prefetch" href="//analytics.google.com" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); }
3. Font Optimization
Proper font configuration affects rendering speed:
/* ✅ Correctly - system fonts fallback */ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; /* next.config.js - Google Fonts optimization */ module.exports = { optimizeFonts: true, experimental: { fontLoaders: [ { loader: '@next/font/google', options: { subsets: ['latin'] } }, ], }, }
Improving FID/INP - Responsiveness
1. Code Splitting and Lazy Loading
Dividing code into smaller parts and loading them on demand:
import dynamic from "next/dynamic"; // ✅ Lazy loading components const HeavyComponent = dynamic(() => import("./HeavyComponent"), { loading: () => <p>Loading...</p>, ssr: false, }); // ✅ Conditional loading const AdminPanel = dynamic(() => import("./AdminPanel"), { ssr: false, }); export default function Page({ user }) { return ( <div> <h1>Homepage</h1> {user?.isAdmin && <AdminPanel />} <HeavyComponent /> </div> ); }
2. JavaScript Optimization
Optimizing event handlers and reducing main thread load:
// ✅ Debounce for input handlers import { useCallback, useMemo } from "react"; import { debounce } from "lodash"; const SearchInput = () => { const debouncedSearch = useMemo( () => debounce(query => { // API call searchAPI(query); }, 300), [], ); const handleChange = useCallback( e => { debouncedSearch(e.target.value); }, [debouncedSearch], ); return <input onChange={handleChange} />; };
3. Web Workers for Heavy Calculations
Moving heavy calculations to Web Workers:
// Component import { useEffect, useState } from "react"; // worker.js self.onmessage = function (e) { const { data } = e.data; // Heavy calculations const result = processLargeDataset(data); self.postMessage(result); }; const HeavyCalculation = ({ data }) => { const [result, setResult] = useState(null); useEffect(() => { const worker = new Worker("/worker.js"); worker.postMessage({ data }); worker.onmessage = e => setResult(e.data); return () => worker.terminate(); }, [data]); return <div>{result}</div>; };
Eliminating CLS - Layout Stability
1. Reserve Space for Images
Preventing layout shifts during image loading:
// ✅ With specified dimensions <Image src="/product.jpg" width={400} height={300} alt="Product" style={{ width: '100%', height: 'auto', }} /> // ✅ With aspect-ratio in CSS <div style={{ aspectRatio: '16/9' }}> <Image src="/banner.jpg" fill alt="Banner" style={{ objectFit: 'cover' }} /> </div>
2. Placeholder for Dynamic Content
Using skeleton loaders for dynamically loaded content:
const ProductCard = ({ productId }) => { const { data: product, isLoading } = useProduct(productId); if (isLoading) { return ( <div className="h-64 w-full animate-pulse rounded-lg bg-gray-200"> <div className="space-y-3 p-4"> <div className="h-4 w-3/4 rounded bg-gray-300"></div> <div className="h-4 w-1/2 rounded bg-gray-300"></div> </div> </div> ); } return ( <div className="h-64 w-full rounded-lg border"> <Image src={product.image} alt={product.name} /> <h3>{product.name}</h3> <p>{product.price}</p> </div> ); };
3. CSS Container Queries
Stable layouts using Container Queries:
/* Stable layouts with container queries */ .card-container { container-type: inline-size; } @container (min-width: 300px) { .card { display: grid; grid-template-columns: 1fr 2fr; gap: 1rem; } }
Next.js Configuration for Performance
next.config.js - Complete Configuration
Full Next.js configuration optimized for performance:
/** @type {import('next').NextConfig} */ const nextConfig = { // Compression compress: true, // Image optimization images: { formats: ["image/avif", "image/webp"], minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days deviceSizes: [640, 750, 828, 1080, 1200, 1920], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], }, // Experimental features experimental: { optimizeCss: true, scrollRestoration: true, }, // Webpack optimizations webpack: (config, { dev, isServer }) => { if (!dev && !isServer) { // Bundle analyzer in production config.optimization.splitChunks.chunks = "all"; } return config; }, // Headers for caching async headers() { return [ { source: "/images/:path*", headers: [ { key: "Cache-Control", value: "public, max-age=31536000, immutable", }, ], }, ]; }, }; module.exports = nextConfig;
Monitoring and Measurements
1. Real User Monitoring (RUM)
Implementing Core Web Vitals monitoring in the application:
// _app.js import { getCLS, getFCP, getFID, getLCP, getTTFB } from "web-vitals"; function sendToAnalytics(metric) { // Send to Google Analytics 4 gtag("event", metric.name, { event_category: "Web Vitals", event_label: metric.id, value: Math.round(metric.name === "CLS" ? metric.value * 1000 : metric.value), non_interaction: true, }); } export function reportWebVitals(metric) { sendToAnalytics(metric); } export default function MyApp({ Component, pageProps }) { return <Component {...pageProps} />; }
2. Performance API
Using Performance API to monitor custom metrics:
// Monitoring custom metrics const measurePageLoad = () => { if (typeof window !== "undefined") { window.addEventListener("load", () => { const navigation = performance.getEntriesByType("navigation")[0]; const loadTime = navigation.loadEventEnd - navigation.fetchStart; console.log(`Page load time: ${loadTime}ms`); // Send to analytics gtag("event", "page_load_time", { value: loadTime, event_category: "Performance", }); }); } };
Key Optimization Areas
Image and media optimization requires using the next/image component with priority flag for hero images, implementing AVIF/WebP formats with fallback, and appropriate sizes and srcset. Lazy loading for images below the fold significantly improves initial loading time.
Fonts and CSS require preloading critical fonts, using system fonts as fallback, inline critical CSS, and removing unused code. These changes directly impact First Contentful Paint.
JavaScript optimization includes code splitting and dynamic imports, tree shaking for libraries, minification and compression, and Service Worker implementation for caching. Each of these techniques contributes to better First Input Delay.
Server and hosting should utilize CDN for static assets, Gzip/Brotli compression, HTTP/2 or HTTP/3, and proper caching headers. These server-side optimizations have a crucial impact on all Core Web Vitals.
Optimization Results
Practical implementation of these techniques delivers measurable results. LCP improves from 4.2 seconds to 1.8 seconds, representing a 57% improvement. FID drops from 180ms to 45ms (75% improvement), and CLS from 0.25 to 0.05 (80% improvement). Overall PageSpeed Score increases from 67 to 94 points, a gain of 27 points.
Summary
Optimizing Core Web Vitals in Next.js requires a systematic approach covering images with proper next/image configuration, JavaScript with code splitting and lazy loading, layout stability with placeholders, and continuous monitoring and fixes. With these techniques, you'll achieve a score above 90 points in PageSpeed and improve Google rankings.
Need help with optimization? Contact us - we'll optimize your Next.js site for maximum performance.
root.pages.case-study.root.pages.case-study.cta-title
Book consultation 30 min.