7 min read

Core Web Vitals for Next.js in 2026: A Practical Checklist

Most Next.js sites that fail Core Web Vitals fail for four or five reasons, and they are the same four or five reasons every time. This is the list I work through, in the order I work through it, with the Next-specific traps that catch people out.

Before any of it: measure field data, not your laptop. Lighthouse on a fast machine over fibre tells you almost nothing about a customer on a mid-range Android on mobile data. Search Console's Core Web Vitals report and the Chrome UX Report are what Google actually grades you on. Lighthouse is for finding causes once you know there is a problem.

1. Find the real LCP element before you optimise anything

Largest Contentful Paint is one specific element, and people routinely optimise the wrong one. Open DevTools, run a Performance trace, and look at the LCP marker — it will tell you exactly which node it was. Nine times out of ten it is a hero image, a heading in a webfont, or a background image set in CSS.

This matters because the fix differs completely. A late hero image is a preload problem. A late heading is a font problem. A background image in CSS is a discovery problem — the browser cannot see it until the stylesheet has parsed, which is why hero backgrounds are usually the worst offender of the three.

2. The Next 16 priority change

If you have been writing Next for a while, your muscle memory says to slap `priority` on the hero image. As of Next 16 that prop is deprecated in favour of `preload`, and the guidance is that most cases want `loading` and `fetchPriority` instead — which are the underlying platform attributes and say what they do:

// Before (Next 15 and earlier)
<Image src="/hero.jpg" alt="" priority />

// Next 16
<Image
  src="/hero.jpg"
  alt=""
  loading="eager"
  fetchPriority="high"
/>

3. Give every image dimensions, or a container that has them

Cumulative Layout Shift is nearly always images and ads without reserved space. `next/image` handles this for you when you pass width and height, or when you use `fill` inside a parent that has its own dimensions. The trap is `fill` inside a parent with no height — the image collapses to nothing until it loads, then pushes everything down.

// Parent needs its own size for fill to reserve space
<div className="relative" style={{ aspectRatio: "16 / 10" }}>
  <Image src={src} alt={alt} fill className="object-cover" />
</div>

Also pass `sizes` on every `fill` image. Without it the browser assumes the image spans the full viewport and downloads the largest candidate in the srcset — a 3840px file for a card that renders at 320px.

4. Fonts: swap, subset, and stop the second request

`next/font` is not optional if you care about this. It self-hosts the font files, removing a DNS lookup and a connection to a third-party host from your critical path, and it generates a size-adjusted fallback so the swap does not shift your layout.

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

export const inter = Inter({
  subsets: ["latin"],   // do not ship glyphs you never render
  display: "swap",      // text is visible during load
  variable: "--font-inter",
});

Two fonts is usually one too many. Every extra family is another render-blocking dependency for the same amount of information on screen.

5. INP is a main-thread problem, not a network one

Interaction to Next Paint replaced First Input Delay as a Core Web Vital in 2024, and it is stricter: it measures the whole interaction through to the next paint, not just the initial delay. A site can load fast and still fail INP badly.

In Next, the usual causes are a large client bundle hydrating, expensive work inside event handlers, and animation that forces layout on every frame. Check what is actually a client component. A page with `"use client"` at the top ships its entire tree to the browser, and it is very easy to end up there by accident because one leaf needed a hook.

  • Push `"use client"` as far down the tree as it will go
  • Animate transform and opacity only — anything else triggers layout or paint per frame
  • Debounce or throttle handlers bound to scroll, resize and input
  • Move genuinely heavy work off the main thread, or off the interaction path entirely

6. Check the bundle before you blame the framework

Look at what is actually being shipped. A date library imported for one format call, an icon set imported wholesale for six icons, an animation library loaded on a page with no animation — these are the normal findings, and they are cheap to fix once you can see them.

One caution from experience: code-splitting is not automatically a win. I split the below-fold sections of a page with `next/dynamic` expecting a smaller initial payload and measured it going the other way — 845 KB to 859 KB, because the page was a client component and Next preloads those chunks for hydration regardless. Splitting only added per-chunk overhead. Measure before and after, and be willing to revert.

The order that matters

If you only do three things: identify the real LCP element and preload it properly, give every image reserved space and a `sizes` attribute, and shrink the client component boundary. That is most of the gap on most Next.js sites, and none of it requires a rewrite.

Related

Got a project with these problems?

Available for remote work worldwide.

Start a conversation →