Skip to content

· 6 min read

Core Web Vitals in 2026: fixing LCP, INP and CLS in Next.js

The three metrics Google measures, what counts as good, and the fixes that move them on a real Next.js site: images, fonts, JavaScript, long tasks and layout shift. With a 30-minute audit routine.

Vishal Solanki · Full-stack developer, Noida

Most slow sites don't have one big problem. They have many small waits, stacked in the wrong order. At Ambak I cut the production JavaScript bundle by about half, and the biggest gains showed up exactly where they matter most: on low-end phones on ordinary mobile connections.

This is the playbook I use to find and fix those waits in a Next.js site, organised around the three numbers Google actually measures.

The three metrics, and what "good" means

MetricWhat it measuresGoodPoor
LCP, Largest Contentful PaintHow long until the biggest thing on screen (usually a hero image or headline) appears2.5 s or lessover 4 s
INP, Interaction to Next PaintHow long the page takes to visibly respond after a tap, click or key press200 ms or lessover 500 ms
CLS, Cumulative Layout ShiftHow much the layout jumps around while loading0.1 or lessover 0.25

Google judges each one at the 75th percentile of real visits, separately for mobile and desktop. INP replaced First Input Delay in March 2024, and it's the one most sites now fail, because it covers every interaction, not just the first.

Field data versus lab data

PageSpeed Insights shows two different things on one page:

  • The top section is field data: real Chrome users over the last 28 days (the Chrome UX Report). This is what Google uses.
  • The Lighthouse score underneath is lab data: one simulated load on a throttled device. It's great for debugging, varies from run to run, and is not what ranks you.

A Lighthouse 100 with failing field data is still failing. A Lighthouse 70 with green field data is fine.

Measure first

Guessing wastes days. Find out which metric fails, on which pages, before you change anything.

  1. Search Console → Core Web Vitals groups your URLs by problem.
  2. PageSpeed Insights names the LCP element and the worst layout shifts on a given page.
  3. Chrome DevTools → Performance shows the LCP element, each interaction's INP and every layout shift, live, as you use the page.
  4. Your own real-user data, sent to analytics from a small client component:
"use client";
import { useReportWebVitals } from "next/web-vitals";

export function WebVitals() {
  useReportWebVitals((metric) => {
    // Send LCP, INP and CLS to your analytics, e.g. as a GA4 event or to your own endpoint.
    window.dataLayer?.push({ event: "web_vitals", name: metric.name, value: metric.value, rating: metric.rating });
  });
  return null;
}

Fixing LCP

1. Find out what the LCP element is. PageSpeed Insights tells you. The fix is completely different for an image than for a heading.

2. If it's an image, serve it with next/image, tell the browser to fetch it early, and give it accurate sizes:

import Image from "next/image";

<Image
  src="/hero.jpg"
  alt="The living room at dusk"
  width={1600}
  height={900}
  sizes="100vw"
  preload
/>

In Next.js 16, preload replaced the old priority prop. Use it on one image per page, the LCP one. Enable modern formats once in next.config.ts with images: { formats: ["image/avif", "image/webp"] }.

3. If it's text, the font is usually the bottleneck. next/font self-hosts and preloads it. Keep only the weights and subsets you actually use.

4. Don't hide the LCP element behind an intro animation. Text that starts at opacity: 0 and waits for JavaScript to fade it in can't count as painted until it's visible. On this site the hero intro only plays on desktop. Phones get the hero immediately.

5. Check the server response. Statically generated pages served from a CDN are fastest. If time to first byte is slow, everything after it is slow too.

Fixing INP

INP is the delay between a user's input and the next frame the browser paints. It's almost always caused by too much JavaScript running on the main thread at the wrong moment.

1. Ship less JavaScript. Keep components on the server unless they need state, effects or browser APIs. Push "use client" as far down the tree as you can, and check what each route actually ships in your bundle analysis. Less code means less to download, parse and hydrate before the page can respond.

2. Show feedback first, then do the heavy work. Break long tasks up so the browser can paint in between:

// Let the browser paint before continuing a long task.
const yieldToMain = () =>
  globalThis.scheduler?.yield?.() ?? new Promise((resolve) => setTimeout(resolve, 0));

async function onApplyFilters() {
  setLoading(true); // visible feedback, painted right away
  await yieldToMain();
  const results = expensiveFilter(allItems); // the slow part
  setItems(results);
}

3. Mark expensive re-renders as non-urgent. For things like filtering a long list as someone types, wrap the update in startTransition (from useTransition) or render from useDeferredValue, so typing stays responsive while the list catches up.

4. Keep animation off the main thread where you can. Animate transform and opacity rather than top, width or height. Don't read layout (getBoundingClientRect, offsetHeight) inside scroll handlers. Load heavy animation code only for the sections that use it, and honour prefers-reduced-motion.

5. Load third-party scripts late. Tag managers, chat widgets and heatmaps often cause the worst INP. On this site, Google Tag Manager loads on the first interaction, or after a few seconds if there isn't one.

Fixing CLS

1. Reserve space for everything that loads later. Give images width and height. Give embeds, videos and ads a fixed aspect-ratio box.

2. Stop fonts from shifting text. next/font generates a fallback font with matching metrics, which removes most of the jump when the real font arrives. For long body text you can go further with display: "optional": on a slow first visit the fallback simply stays, rather than swapping mid-read. This site uses that for its body font.

3. Never insert content above what's already on screen. Cookie bars, promo banners and "related" blocks should overlay the page or have space reserved for them.

4. Animate with transform. Changing height, margin or top moves everything below. A transform or an opacity fade doesn't.

A 30-minute audit routine

  1. Open Search Console → Core Web Vitals. Note which metric fails, on mobile or desktop, and which URL groups.
  2. Run PageSpeed Insights on one URL from each failing group. Write down the LCP element and the largest layout shifts.
  3. Open the page in Chrome DevTools with CPU throttling at 4× and a "Fast 4G" network. Record a Performance trace while loading the page and tapping the main controls.
  4. In the trace, look for long tasks (over 50 ms) right after load and right after each tap. Note what code they belong to.
  5. Fix the single biggest item, deploy, and wait. Field data trails your changes by up to 28 days, so give each fix time to show.

FAQ

Do Core Web Vitals affect Google rankings?

Yes, as part of Google's page experience signals. Relevance and content quality still matter more, so a fast page with the wrong content won't outrank a slower page that answers the question. Speed is what keeps people on the page once they arrive.

Why does PageSpeed Insights give a different score every time?

The Lighthouse score is a single simulated run, and it varies with server response time and test conditions. Judge progress by the field data at the top of the report and in Search Console.

Do I need a Lighthouse score of 100?

No. Aim to pass all three Core Web Vitals in field data, on mobile, for your important pages. Past that point, your time is usually better spent on content.


Is your site slow on phones, or failing Core Web Vitals in Search Console? I find the cause and fix it.

  • Core Web Vitals
  • Performance
  • Next.js
  • INP