Mattéo Rogier
Optimize Core Web Vitals with Next.js: the complete technical guide (2026)
Tech

Optimize Core Web Vitals with Next.js: the complete technical guide (2026)

Mattéo ROGIER··9 min read

Introduction

In 2026, Core Web Vitals are no longer just a Google recommendation — they're a confirmed ranking factor that directly impacts your visibility. According to Chrome UX Report data, 53% of French websites still don't pass the "good" threshold on all three key metrics.

If you're using Next.js (and you should — it's the most performant React framework for SEO), you have a significant advantage. But you need to know how to leverage it. In this guide, I'll show you concretely how to optimize each metric with copy-paste-ready code.

The 3 metrics that matter in 2026

Quick recap:

  • LCP (Largest Contentful Paint): loading time of the largest visible element. Target: < 2.5 seconds.
  • CLS (Cumulative Layout Shift): visual stability. Target: < 0.1.
  • INP (Interaction to Next Paint): interaction responsiveness. Target: < 200ms. INP replaced FID since March 2024.

Each of these metrics influences your Google ranking. Let's see how to optimize them one by one.

1. Optimize LCP: load what matters first

LCP is often dragged down by unoptimized images or render-blocking fonts. With Next.js, here's the solution.

Use the Next.js Image component

The next/image component automatically handles lazy loading, resizing, and WebP/AVIF format:

`tsx

import Image from "next/image";

export default function Hero() {

return (

src="/images/hero-banner.jpg"

alt="Freelance web developer at work"

fill

priority // ← Disables lazy loading for the LCP image

sizes="100vw"

className="object-cover"

quality={85}

/>

Your website, optimized for performance

);

}

`

Key points:

  • priority tells Next.js to preload this image (automatically adds a ).
  • sizes="100vw" lets the browser choose the right image size for the screen.
  • quality={85} reduces weight without visible loss.

Preload your fonts with next/font

Google Fonts often block rendering. Next.js solves this natively:

`tsx

// app/layout.tsx

import { Inter } from "next/font/google";

const inter = Inter({

subsets: ["latin"],

display: "swap", // ← Shows text immediately

preload: true,

variable: "--font-inter",

});

export default function RootLayout({ children }: { children: React.ReactNode }) {

return (

{children}

);

}

`

Result: 0ms of font-blocking, because Next.js self-hosts them automatically and applies font-display: swap.

Pro tip: Server Components for above-the-fold content

With the Next.js App Router, your components are Server Components by default. HTML is generated server-side and sent immediately:

`tsx

// app/page.tsx — Server Component by default, no "use client"

async function getHeroData() {

// This request runs server-side at build time

const data = await fetch("https://api.example.com/hero", {

next: { revalidate: 3600 }, // ISR: regenerates every hour

});

return data.json();

}

export default async function Home() {

const hero = await getHeroData();

return (

{hero.title}

{hero.subtitle}

);

}

`

No client-side JavaScript for initial render = drastically reduced LCP.

2. Eliminate CLS: every pixel in its place

CLS is caused by elements that "shift" during loading. The usual culprits: images without dimensions, fonts that change size, and dynamically injected content.

Reserve space for images

`tsx

// ❌ Bad: no dimensions = guaranteed CLS

Photo

// ✅ Good: explicit dimensions + CSS aspect-ratio

src="/photo.jpg"

alt="Project photo"

width={800}

height={450}

className="aspect-video w-full h-auto"

/>

`

The next/image component automatically calculates the ratio and reserves space. CLS: 0.

Handle dynamic content with Suspense

When a component loads client-side data, use Suspense with an identically-sized skeleton:

`tsx

import { Suspense } from "react";

function TestimonialSkeleton() {

return (

);

}

export default function Page() {

return (

What our clients say

}>

);

}

`

The skeleton has exactly the same height as the final component. Result: zero visual shift.

The golden rule for ads and embeds

If you integrate iframes (YouTube, Google Maps, etc.), always define a container with fixed dimensions:

`tsx

src="https://www.youtube.com/embed/VIDEO_ID"

className="absolute inset-0 h-full w-full"

loading="lazy"

title="Presentation video"

/>

`

3. Tame INP: ultra-responsive interactions

INP measures the time between a user interaction (click, tap, keystroke) and the visual update. It's the hardest metric to optimize.

Use useTransition for non-urgent updates

`tsx

"use client";

import { useState, useTransition } from "react";

export default function SearchFilter({ items }: { items: Item[] }) {

const [query, setQuery] = useState("");

const [filtered, setFiltered] = useState(items);

const [isPending, startTransition] = useTransition();

function handleSearch(value: string) {

setQuery(value); // ← Urgent update (responsive input)

startTransition(() => {

// ← Non-urgent update (filtering can wait)

const results = items.filter((item) =>

item.name.toLowerCase().includes(value.toLowerCase())

);

setFiltered(results);

});

}

return (

type="text"

value={query}

onChange={(e) => handleSearch(e.target.value)}

placeholder="Search..."

className="w-full rounded border p-3"

/>

{filtered.map((item) => (

{item.name}

))}

);

}

`

useTransition tells React: "the input is priority, filtering can happen in the background". The user feels zero lag while typing.

Load heavy components dynamically

`tsx

import dynamic from "next/dynamic";

// The map component only loads when it's visible

const Map = dynamic(() => import("@/components/Map"), {

loading: () => (

),

ssr: false, // No server render for heavy interactive components

});

export default function ContactPage() {

return (

Find us

);

}

`

4. Measure your results: the monitoring setup

Optimizing without measuring is shooting blind. Here's how to track your Core Web Vitals in production with Next.js:

`tsx

// app/layout.tsx

import { SpeedInsights } from "@vercel/speed-insights/next";

import { Analytics } from "@vercel/analytics/react";

export default function RootLayout({ children }: { children: React.ReactNode }) {

return (

{children}

);

}

`

If you're not on Vercel, use the web-vitals API directly:

`tsx

// app/components/WebVitals.tsx

"use client";

import { useReportWebVitals } from "next/web-vitals";

export function WebVitals() {

useReportWebVitals((metric) => {

// Send to your analytics (Google Analytics, Plausible, etc.)

console.log(metric.name, metric.value);

if (typeof window.gtag === "function") {

window.gtag("event", metric.name, {

value: Math.round(metric.value),

event_label: metric.id,

non_interaction: true,

});

}

});

return null;

}

`

Summary checklist

Before going to production, verify:

  • ✅ LCP images with priority and sizes defined
  • ✅ Fonts loaded via next/font with display: swap
  • ✅ Server Components for all above-the-fold content
  • ✅ Explicit dimensions on all images and iframes
  • Suspense + skeletons for dynamic content
  • useTransition for filtering/search interactions
  • dynamic() for heavy non-critical components
  • ✅ Production monitoring enabled

Conclusion

Core Web Vitals aren't a technical chore — they're a competitive advantage. A site that loads in under 2 seconds, doesn't shift, and responds instantly to clicks converts 2 to 3 times better than a slow site (source: Google/Deloitte, 2024).

With Next.js and the techniques in this guide, you can reach a Lighthouse score of 95-100 without sacrificing design or features.

Need a developer who masters these optimizations? I build ultra-performant Next.js sites for my clients. Book a call to discuss — the initial performance audit is free.

Have a project? Let's talk.

Book a free, no-obligation call to discuss your goals.

Book a free call

Related articles