How to Pass Core Web Vitals in Next.js Without Dumbing Down the Design
DevelopmentPerformance

How to Pass Core Web Vitals in Next.js Without Dumbing Down the Design

Muhammad Makashif
Muhammad Makashif
2025-12-10 7 min read

A premium website is not the one with the most effects. It is the one that feels instant on a mid-range phone and still looks extraordinary on a studio monitor. My own site runs a GPU fluid simulation, a procedural deep-space nebula and hero smoke on Next.js 16 and React 19, so balancing visual ambition against Core Web Vitals is something I deal with in practice, not in theory.

What "fast" means in 2026

Google's Core Web Vitals are the closest thing the web has to a shared definition of fast. A page passes when at least 75% of real visits meet all three "good" thresholds:

  • Largest Contentful Paint (LCP): 2.5 seconds or less. How quickly the main content appears.
  • Interaction to Next Paint (INP): 200 milliseconds or less. How quickly the page responds to taps, clicks and key presses. INP replaced First Input Delay as a Core Web Vital in March 2024.
  • Cumulative Layout Shift (CLS): 0.1 or less. How much the layout jumps around while people use it.

Two things trip people up. First, these are judged on field data from real Chrome users (the Chrome UX Report), not on a Lighthouse run. Lighthouse is a lab tool: great for debugging, but it cannot measure INP and uses Total Blocking Time as a stand-in. A perfect Lighthouse score with failing field data is still failing. Second, the 75th percentile means your slowest quarter of visitors decides the result.

PageSpeed Insights shows field and lab data side by side, Search Console groups failing URLs, and Next.js's useReportWebVitals hook sends metrics to your own analytics.

Ship less JavaScript with Server Components

In the App Router, every component is a Server Component unless its file starts with 'use client'. Server Components render to HTML and send no component code to the browser. That makes 'use client' the most important performance decision in a Next.js codebase, because everything a client file imports becomes part of the client bundle.

The rules I follow:

  • Push the client boundary down to the leaves. A product page should be a Server Component; only the quantity picker and the add-to-cart button need to be client code.
  • Pass server-rendered content through as children. A client wrapper such as an animated reveal, a modal or a tab set can receive Server Components as children, and they stay server-rendered.
  • Keep heavy work on the server. Markdown parsing, syntax highlighting, date formatting and data shaping belong in Server Components, where their libraries never reach the browser.
// Reveal.tsx: a tiny client wrapper
'use client';
import { motion } from 'framer-motion';

export function Reveal({ children }: { children: React.ReactNode }) {
  return (
    <motion.div initial={{ y: 24 }} animate={{ y: 0 }}>
      {children}
    </motion.div>
  );
}

// page.tsx: stays a Server Component
import { Reveal } from './Reveal';
import { Pricing } from './Pricing'; // rendered on the server, no client JS

export default function Page() {
  return (
    <Reveal>
      <Pricing />
    </Reveal>
  );
}

Notice that the reveal animates position, not opacity. That detail matters for LCP.

LCP: get the hero on screen fast

Start by finding the LCP element; the Performance panel in Chrome DevTools labels it.

  • Load the hero image eagerly and at high priority. next/image lazy-loads by default, which is right for everything except the hero. In Next.js 16 the old priority prop is deprecated, and the docs recommend loading="eager" or fetchPriority="high" for the LCP image, with the new preload prop kept for special cases.
  • Always set sizes on responsive or fill images. Without it, the browser assumes the image spans the whole viewport and may download a far larger file than the layout needs.
  • Do not fade in your LCP element. Chrome ignores elements at zero opacity when choosing LCP candidates, so a headline that starts at opacity 0 only counts once its animation begins, which is often after hydration. Animate transform instead, or start from a visible state.
  • Keep the LCP in HTML, not in a canvas. A WebGL canvas is not an LCP candidate and should never delay one. On my site, the smoke behind the hero is loaded separately, after the hero content is already on the page.
  • Load fonts with next/font. It self-hosts the files and generates a size-adjusted fallback so the font swap shifts less. Ship only the weights you use.
  • Cut server time. A prerendered page served from a CDN beats any amount of front-end tuning. With Cache Components enabled in Next.js 16, a route can serve a static shell immediately and stream its dynamic parts as they resolve, so one personalised widget no longer forces the whole page to render per request.

INP: keep the main thread free

INP looks at interactions throughout the visit and reports roughly the worst one. Poor INP almost always means the main thread was busy when the user acted: hydrating a large bundle, running a heavy event handler or re-rendering a big component tree.

  • Hydrate less. Every kilobyte of client JavaScript is downloaded, parsed, executed and hydrated. Moving code to Server Components is the biggest INP fix most sites will ever get.
  • Mark non-urgent updates as transitions. Wrapping an expensive state update in startTransition lets React keep the keystroke or click that caused it responsive, for example when filtering a long list.
  • Defer third-party scripts. Load chat widgets, heatmaps and tag managers with next/script and the lazyOnload strategy, or only after consent and interaction.
  • Virtualise long lists, and never read layout values such as offsetHeight inside scroll handlers.
  • Let the compiler memoise. Next.js 16 supports the React Compiler, which removes most hand-written useMemo and useCallback busywork.

To find the culprit, record a slow interaction in the DevTools Performance panel with CPU throttling enabled and look for long tasks. In the field, the Long Animation Frames API can attribute slow frames to specific scripts.

CLS: reserve space for everything

  • Give every image, video and embed explicit dimensions or an aspect-ratio. next/image requires width and height, or fill inside a sized container, for exactly this reason.
  • Show cookie banners and promos as overlays, never by pushing content down.
  • Make skeletons the same size as the content they replace.
  • Animate with transform. Transforms never cause layout shift; animating top, height or margin can.

Heavy visuals without the heavy penalty

This is where "premium" sites usually lose. Five rules keep ambition and speed compatible:

  1. Keep 3D libraries out of the initial bundle. Load WebGL effects with next/dynamic and server rendering disabled, from a Client Component, so the page renders and becomes interactive first. Every Three.js effect on my site is lazy-loaded this way.
  2. Make the effect decorative. If the canvas fails to start, the content must still work.
  3. Adapt to the device. My site runs an adaptive quality governor that measures real frame rates and lowers simulation quality only when the page genuinely stalls. On devices that still struggle, it switches effects off one at a time instead of degrading everything at once.
  4. Respect reduced-motion preferences. Visitors who ask their OS to reduce motion should get a calm version of the page, not just a slower one.
  5. Clean up. Dispose GPU resources and release WebGL contexts when a canvas unmounts.

A quick audit checklist

  1. Check field data in PageSpeed Insights or Search Console before touching code.
  2. Confirm the mobile LCP element is HTML text or an eagerly loaded image.
  3. Run the bundle analyzer and list every large library in a client chunk.
  4. Move 'use client' down the tree until only interactive leaves remain.
  5. Defer or remove every third-party script you can.
  6. Record your most common interactions with CPU throttling and fix the long tasks.
  7. Reserve space for every image, embed and late-arriving element.
  8. Re-check field data after four weeks; the Chrome UX Report is a rolling 28-day window.

Key takeaways

  • Core Web Vitals are judged on real-user data at the 75th percentile: LCP of 2.5 s or less, INP of 200 ms or less, CLS of 0.1 or less.
  • Server Components are the biggest single lever; keep 'use client' at the leaves of the tree.
  • In Next.js 16, use loading="eager" or fetchPriority="high" for the hero image instead of the deprecated priority prop.
  • Never fade in the LCP element, and never make visitors wait on a canvas.
  • Rich visuals are compatible with good scores when they are lazy-loaded, decorative and adaptive.

If your Next.js site is failing Core Web Vitals, my Core Web Vitals fixes start from field data rather than guesswork, and the same approach drives my React performance optimization work for dashboards and apps.

Planning a project? Book a free discovery call — I'll tell you honestly what it needs.

#Next.js#Core Web Vitals#React Server Components#Performance#INP#LCP

Keep Reading

EXPLORE ALL JOURNAL ENTRIES