BlogWeb Development

Core Web Vitals Optimization Guide for Next.js

M
Mousa H.
|14 min readJan 22, 2026
Developer optimizing Core Web Vitals scores in a Next.js application

LCP, FID, CLS — how to diagnose and fix every metric. Real examples from sites we’ve optimized to 95+ scores.

What Core Web Vitals Actually Measure — and Where the Data Comes From

Core Web Vitals are Google’s three field metrics for real-user experience: Largest Contentful Paint, Interaction to Next Paint, and Cumulative Layout Shift. LCP measures how long the biggest visible element — usually a hero image or headline — takes to render, and Google’s threshold for “good” is two and a half seconds. INP measures how long the page takes to visually respond after a user taps, clicks, or types, with a threshold of two hundred milliseconds; it replaced First Input Delay in March 2024, and it’s a much stricter metric because it scores every interaction on the page, not just the first one. CLS measures how much the layout jumps around while loading, with a threshold of 0.1 on its unitless scale.

Two details trip up almost everyone who starts optimizing. First, the assessment that matters for search is field data — the Chrome User Experience Report, collected from real Chrome users over a rolling twenty-eight-day window and judged at the seventy-fifth percentile. A perfect Lighthouse score on your office fibre connection tells you nothing about the phone-on-transit users who set your real numbers. Second, because the window rolls over twenty-eight days, fixes take weeks to show up in Search Console’s Core Web Vitals report. Ship the fix, verify it in lab tools immediately, then wait for the field data to catch up.

This guide assumes you’re on the App Router. Most of the techniques have Pages Router equivalents, but the architecture that makes the biggest difference — React Server Components — is an App Router story.

Why Next.js Helps — and Where It Quietly Hurts

Next.js gives you a head start on every vital. Pages can be prerendered to static HTML or rendered on the server, so the browser receives meaningful content instead of an empty shell waiting for JavaScript. The image component handles responsive sizing, modern formats, and lazy loading. The font system self-hosts your fonts. Code is split per route automatically. A static Next.js page served from a CDN edge node is about as fast a starting point as the web offers.

But the framework also hands you specific ways to lose. The most common is treating it like a client-side React app: marking everything with the use client directive, pulling in heavy component libraries, and shipping hundreds of kilobytes of JavaScript that the browser must download, parse, and execute before the page responds to input. That’s how a server-rendered site ends up with a failing INP. The second is misusing the image component — leaving the hero image lazy-loaded, which actively delays LCP, or rendering it without proper sizing so it shifts the layout. The third is uncached server rendering: a dynamic page that waits on a slow database query or third-party API on every request inherits that latency directly into LCP, because nothing can paint before the HTML arrives.

The honest framing: Next.js raises your ceiling dramatically, but it doesn’t enforce a floor. The rest of this guide is about not digging through it, metric by metric.

Fixing LCP: The Hero Image and the Server Response

LCP has two halves: how fast the HTML arrives, and how fast the largest element renders once it does. Most teams only work on the second half and wonder why they plateau.

Start with the element itself. Identify your actual LCP element in Chrome DevTools — it’s often not what you assume. If it’s an image, render it with the Next.js image component and set the priority prop. That single change does three things: it disables lazy loading for that image, marks it with high fetch priority so the browser requests it before less important resources, and injects a preload hint so discovery happens immediately rather than after the page’s CSS and JavaScript settle. Forgetting priority on the hero is the single most common LCP mistake we see in audits; lazy loading is the right default for images below the fold and exactly the wrong behaviour for the one element Google times. Also give the component accurate sizes information so mobile devices download a phone-sized file instead of the desktop original.

If your LCP element is text, the bottleneck is usually font loading — covered in the next section — or a background image applied in CSS, which the preloader can’t discover early. Move decorative hero backgrounds into real image elements where possible.

Then attack the first half: time to first byte. Static pages and pages using incremental static regeneration serve prebuilt HTML from the edge, which is the strongest TTFB you can buy. For pages that must render dynamically, cache the data layer aggressively — Next.js lets you cache individual fetch calls with revalidation windows — and watch out for a single slow upstream call serializing the whole response. On Vercel, static and cached responses are served from the edge network closest to the visitor, which matters more than people expect for a Toronto business with traffic spread across Canada.

Fonts: Fast Text Without the Flash

Web fonts sit at the intersection of two vitals. Load them slowly and your text-based LCP suffers or your headline renders in a fallback font first; load them carelessly and the swap from fallback to web font reflows the page and charges you CLS.

The App Router answer is the built-in font system. Importing a Google font through next/font downloads it at build time and serves it from your own domain, which removes the third-party connection entirely — no DNS lookup, no separate TLS handshake to a fonts CDN, no external request in the critical path. The CSS is inlined, and the font files are immutable and cached.

The underrated part is what it does for layout shift. The font system automatically calculates a size-adjusted fallback font — it tweaks the metrics of a system font so its character widths and line heights approximate your web font. Text renders immediately in the adjusted fallback, and when the real font arrives, the swap happens with little or no visible reflow. That’s the mechanism that lets you use font-display swap behaviour, which keeps text visible and LCP fast, without paying the traditional CLS penalty for it.

Practical rules: load only the weights and subsets you actually use, because every weight is a separate file; declare fonts once in the root layout rather than per page; and prefer variable fonts when a design needs several weights, since one variable file usually beats three static ones. If your brand font comes from a paid foundry, the same system handles local font files with the same fallback adjustment.

Fixing INP: Ship Less JavaScript, Run Less JavaScript

INP is where modern React sites fail, and the cause is almost always the same: too much JavaScript executing on the main thread. Every interaction the browser handles has to wait for whatever else the thread is doing — hydration, third-party scripts, expensive re-renders. The fix is not micro-optimizing event handlers; it’s shipping and running less code.

The App Router’s biggest gift here is that Server Components are the default. A component without the use client directive renders on the server and contributes zero JavaScript to the bundle — no download, no parse, no hydration work. The discipline is keeping it that way. The use client directive marks a boundary, and everything imported below that boundary becomes client code, so one careless directive near the top of a layout can drag half the component tree into the bundle. Push client boundaries down to the leaves: the page, the sections, the cards stay on the server; the interactive bits — the menu toggle, the carousel controls, the form — become small client islands. A pattern worth internalizing is passing server-rendered children through a client wrapper, so an interactive shell doesn’t convert its contents.

For client code you genuinely need, load it when it’s needed. Next.js supports lazy-loading components with next/dynamic, which is the right treatment for anything triggered by user action or hidden at load — modals, chat widgets, video players, map embeds, anything below the fold with heavy dependencies.

Then audit third parties, because in our experience they dominate real-world INP more often than first-party code does. Load analytics and marketing tags through the Next.js script component with the after-interactive or lazy-on-load strategy so they stay out of the critical path, ruthlessly question every tag in the tag manager, and replace embedded widgets with lightweight facades — a static preview that loads the real thing on click. One chat widget can cost more main-thread time than your entire application code.

Fixing CLS: Reserve Every Pixel Before It Arrives

Cumulative Layout Shift is the most mechanical vital to fix, because every shift has the same root cause: content arrived without space reserved for it, so the browser pushed everything else out of the way.

Images are the classic offender, and Next.js largely solves them for you — the image component requires width and height, or a fill mode inside a sized container, and uses them to reserve the element’s exact aspect ratio before a single byte of the file arrives. The shifts that survive come from images outside the component, from CSS that overrides the intrinsic sizing, or from containers whose own height depends on the image. The modern CSS aspect-ratio property is the general-purpose tool for the same job on video embeds, iframes, and ad slots: give the container its final proportions up front and let the content load into a box that already exists.

The sneakier sources are dynamic. Client components that render nothing on the server and pop into existence after hydration — cookie banners, announcement bars, personalized content — shift everything below them; either reserve their space, render them as overlays that don’t displace layout, or render a placeholder of identical height from the server. Content that depends on client-side data fetching should show a skeleton sized like the real thing, not appear from nowhere. And remember that CLS is measured across the whole page lifetime, not just load: an accordion that pushes content is fine because it follows a user interaction, but anything that moves on its own schedule — a late-loading ad, a rotating banner that changes height — keeps charging you. In practice, a Next.js site with disciplined image usage and the built-in font system should sit comfortably under the 0.1 threshold; if it doesn’t, record the page load with the performance panel and the offending element is usually visible within minutes.

Streaming and Suspense: Making Dynamic Pages Feel Static

The App Router’s streaming model deserves its own section because it changes the economics of dynamic pages. Traditionally, a server-rendered page was all-or-nothing: the visitor stared at a blank screen until the slowest data dependency resolved. Streaming breaks that coupling.

The mechanics: wrap slow parts of a page in React Suspense boundaries, or add a loading file to a route segment, and Next.js sends the surrounding HTML immediately while the slow sections stream in as they’re ready. The shell — navigation, headline, hero — paints right away; the dashboard widget waiting on a sluggish API arrives a moment later, replacing its fallback in place.

Used well, this directly improves measured vitals, not just perceived speed. If your LCP element lives in the shell — and on a marketing site it should — its paint time stops depending on the slowest query on the page. The page becomes interactive sooner because the browser starts work earlier.

The craft is in placing boundaries deliberately. Wrap the things that are actually slow and genuinely below the primary content: reviews pulled from a third-party API, related-content modules, account-specific data. Don’t wrap the hero, and don’t suspend the LCP element itself — a fallback spinner that gets replaced by the real content is both a worse experience and a potential layout shift if the fallback isn’t sized to match. Every Suspense fallback should obey the CLS rule from the previous section: same dimensions as what replaces it.

For pages that are mostly static with a small dynamic island — a personalized greeting, a cart count — consider whether the dynamic part can move client-side or behind a Suspense boundary so the rest of the page can be prerendered and cached at the edge. The fastest server render is the one you didn’t do.

Bundle Analysis, Monitoring, and Keeping the Score

Everything above gets you to good vitals once. Staying there is a process problem, and it has tooling.

Start with visibility into what you ship. The official bundle analyzer plugin renders an interactive map of every chunk in your build, and the first look is almost always humbling: a date library imported whole for one formatting call, a charting package bundled into a page that renders one sparkline, two icon libraries because two developers had different habits. Heavy dependencies get three options — replace them with something smaller, load them dynamically so they leave the critical path, or move the work into a Server Component so the client never sees it. Re-run the analyzer after dependency upgrades; bundles grow silently.

Then instrument the field, because that’s where you’re judged. Vercel’s Speed Insights reports real-user vitals per route, which turns “the site is slow” into “INP regressed on the contact page after the last deploy.” If you’re not on Vercel, the web-vitals library feeds the same per-route data into whatever analytics you run, and Next.js exposes a reporting hook for exactly this. Cross-check against Search Console’s Core Web Vitals report and PageSpeed Insights, which show the CrUX field data Google actually uses — remembering the twenty-eight-day lag.

Finally, make regressions expensive to ship. Lighthouse CI in the deploy pipeline with budgets on bundle size and lab metrics catches the worst offenders before they reach users. The pattern we’ve settled on at SearchPod after optimizing a lot of client builds: lab tools to diagnose and verify, field data to judge, and a budget in CI so the next feature, the next marketing tag, and the next dependency upgrade all have to answer to the same numbers. Core Web Vitals aren’t a launch checklist — they’re a property you maintain, and Next.js makes maintaining them about as cheap as it currently gets.

Want help implementing this?

Get a free proposal for your web development setup. We’ll show you exactly where the opportunities are.

Get Free Proposal

No upfront fees. No long contracts. If you’re not satisfied after the first 30 days, you don’t pay.

Get Free Proposal
Get Free ProposalCall