🇬🇧translation is currently inBeta

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.

Adam Noszczyński
10 min czytania
SEO

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.

Ailo client logoCledar client logoMiohome client logoPlenti client logoWebiso client logo+4
I've been working on projects for clients for 11 years

Tagi:

Core Web Vitals
Next.js
PageSpeed
SEO
Optimization