Asia/Karachi
BlogOctober 6, 2026

Core Web Vitals Explained: LCP, INP and CLS

What the three metrics measure and how to fix them in Next.js
Fawaz Israr
Core Web Vitals Explained: LCP, INP and CLS
Core Web Vitals are three measurements of how a page feels to real visitors. LCP (Largest Contentful Paint) is how quickly the main content shows up, INP (Interaction to Next Paint) is how quickly the page reacts to taps and clicks, and CLS (Cumulative Layout Shift) is how much the layout jumps while it loads. To improve them on a React or Next.js site, you make the hero image load first, reserve space for anything that arrives late, and keep heavy JavaScript away from the first load. I am a final-year Computer Science student who does SEO work. The examples below use Next.js, the same framework as this portfolio. Vitals are one signal among many. LCP measures how long the largest visible element in the viewport takes to render. Often that element is a hero image, a big heading or a block of text. It is a loading metric, so it answers the question "when did the page look ready?" INP looks at every click, tap and key press during a visit and reports roughly the slowest one, counted from the input until the browser paints the next frame. It is the successor to First Input Delay, which only looked at the first interaction. A page can load fast and still have a bad INP if a button handler blocks the main thread. CLS adds up unexpected movement of visible elements. Each shift gets a score based on how much of the screen moved and how far, and the reported value comes from the worst burst of shifts, not the whole visit. You have felt a bad one: you go to tap a link and an image loads above it, pushing the link away. The good thresholds from web.dev are:
  • LCP: 2.5 seconds or less
  • INP: 200 milliseconds or less
  • CLS: 0.1 or less
A page passes when all three are good at the 75th percentile of page loads, measured separately for mobile and desktop. The 75th percentile only makes sense with field data. Field data comes from real visitors on real phones and connections. If at least three out of four page loads meet the threshold, the metric counts as good. One fast run on your laptop cannot show that. Lab data comes from a controlled test, such as Lighthouse in Chrome DevTools. It is repeatable and gives you a trace to inspect, which makes it the right tool for finding out why something is slow. It cannot tell you how your audience actually experiences the page. Here is how the usual tools line up:
  • PageSpeed Insights shows field data at the top (when your URL has enough traffic) and a Lighthouse lab run below it.
  • The Core Web Vitals report in Search Console groups your URLs by field status, so you can see which templates fail.
  • In its default page-load run, Lighthouse reports LCP and CLS but not INP, because nothing interacts with the page. Total Blocking Time is the lab proxy that web.dev suggests for catching interactivity problems.
A new portfolio or a small blog often has no field data at all, and PageSpeed Insights will say so. In that case use Lighthouse to fix obvious problems and wait for real data to accumulate. Start by finding the LCP element. PageSpeed Insights and the Performance panel both name it. On a portfolio it is usually the hero image or the profile photo, and that image is the one to optimize first. next/image lazy loads images by default, which is the right choice for anything below the fold and the wrong one for your LCP image. Mark the hero so Next.js preloads it. In Next.js 16 the prop for that is preload, and the older priority prop is deprecated.
Tsx
import Image from "next/image";
import hero from "./hero.jpg";

export function Hero() {
  return (
    <Image
      src={hero}
      alt="Screenshot of the shop dashboard on a laptop"
      preload
      sizes="(min-width: 1024px) 960px, 100vw"
    />
  );
}
Importing the file statically lets Next.js read its width and height. The sizes value tells the browser how wide the image will really be, so it does not download a 2000 pixel file for a 400 pixel slot. Fonts are the next common delay. next/font self-hosts the font files and preloads them, so you do not pay for a separate request to Google's servers. If you load a font some other way, preload the file you actually use with a link rel="preload" tag, as="font", type="font/woff2" and the crossorigin attribute. Fonts need that attribute even when they come from your own domain. Last, do not hide the hero behind client-side work. If the image only appears after a React effect runs, the browser cannot find it early, so render it in a Server Component. Most CLS problems come from content that shows up without reserved space. The fixes are mostly a matter of telling the browser the size early.
  • Give every image a width and height, or import it statically so Next.js does it for you. With fill, put the image inside a parent that has a fixed size or aspect ratio.
  • Reserve room for embeds, ads and cookie banners. If a banner pushes the page down after load, make it overlay the page or give it a fixed slot.
  • Make loading skeletons the same height as the content that replaces them.
  • Animate with transform and opacity rather than properties such as height or top, which move other elements.
Web fonts can cause a small shift when the fallback font swaps for the real one, because the letters have different widths. next/font generates an adjusted fallback to keep that swap small, which is one more reason to use it. INP gets worse when the main thread is busy, and the main thread is busiest when a lot of JavaScript is running. Two habits help: ship less of it up front, and make each interaction do less work. Split heavy client code that is not needed on first load, such as a chart, a rich text editor or a map, with a dynamic import.
Tsx
"use client";

import dynamic from "next/dynamic";

const SalesChart = dynamic(() => import("./SalesChart"), {
  loading: () => <p>Loading chart...</p>,
});

export function Report() {
  return <SalesChart />;
}
The chart code now arrives as a separate chunk instead of sitting in the main bundle. For something that only appears after a click, such as a modal, you can call import() inside the click handler instead. Third-party scripts are a frequent cause of slow interactions, because they run on the same thread as your code. Load them with next/script and a late strategy.
Tsx
import Script from "next/script";

export function Analytics() {
  return (
    <Script
      src="https://example.com/widget.js"
      strategy="lazyOnload"
    />
  );
}
The lazyOnload strategy waits until the browser is idle after the page has loaded. It suits chat widgets and similar extras. Analytics may need the default afterInteractive strategy, so decide per script. Inside your components, look for state that lives too high in the tree and re-renders large lists on every keystroke or click. If you keep cart state in Redux, as in my shopping cart with Redux Toolkit post, select only the fields each component needs so one change does not re-render everything. For updates that can wait, such as filtering a long list while someone types, wrap the state change in startTransition so the input stays responsive. Lab tools show a single run. To see what happens on your own visitors' devices, Google publishes the small web-vitals library with one function per metric: onLCP, onINP and onCLS. Install it with npm install web-vitals, then add a client component.
Tsx
"use client";

import { useEffect } from "react";
import { onCLS, onINP, onLCP, type Metric } from "web-vitals";

function report(metric: Metric) {
  console.log(metric.name, metric.value, metric.rating);
}

export function WebVitals() {
  useEffect(() => {
    onLCP(report);
    onINP(report);
    onCLS(report);
  }, []);

  return null;
}
Render WebVitals once in your root layout. Each callback receives an object with the metric name, its value, a rating of good, needs improvement or poor, and an id. The values can arrive late: they are often reported when you switch tabs or leave the page, so click around and then change tabs before you check the console. Logging is for development. In production you would send the same object to an endpoint with navigator.sendBeacon and work out the 75th percentile yourself. The library also has an attribution build that tells you which element or interaction caused a bad value. Next.js has a useReportWebVitals hook in next/web-vitals that does a similar job if you prefer something built in. Google recommends good Core Web Vitals, and its documentation says they align with what its core ranking systems seek to reward, together with other aspects of page experience. That is a reason to care, though nobody can promise that fixing them will move a particular page up. My own reading is that a page the visitor finds relevant will not rise just because the numbers turn green, and a fast page with thin content will not rank for much. Treat vitals as one signal among many and as a way to be less annoying to the people who do arrive. Jumpy layouts annoy visitors whatever Google does. They also sit beside the basics. Check the setup in technical SEO for Next.js with metadata, sitemap and canonical URLs, and go through the on-page SEO checklist for a developer portfolio before you spend a weekend shaving milliseconds. If you want a first step, run PageSpeed Insights on your slowest page, write down the LCP element it names, and fix that one image or font before touching anything else. Cover photo from Unsplash.
Share this post: