Next.js Performance Guide 2026: Fix LCP, CLS, and INP in the App Router Era

Next.js is great. It is genuinely good at a lot of things. But it is also very easy to build a Next.js app that has terrible Core Web Vitals, because the framework gives you a lot of power and several

Glowing Next.js triangle logo surrounded by performance charts and React atom wireframes on dark navy background

Next.js is great. It is genuinely good at a lot of things. But it is also very easy to build a Next.js app that has terrible Core Web Vitals, because the framework gives you a lot of power and several of those powers fire directly at your own foot if you are not careful.

This guide is for React and Next.js developers specifically. If you are on WordPress or Shopify, the other guides have more relevant advice for your stack. But if you are using App Router, Pages Router, or somewhere in between, and your CWV are struggling, read on.

The Most Common Next.js CWV Mistakes

Before we get into the fixes, here is a list of the patterns that cause problems most often. You will probably recognize some of these.

Let us go through each one with fixes.

next/image: The LCP Lifesaver (When Used Correctly)

The next/image component handles WebP conversion, responsive srcsets, and lazy loading automatically. But it has two critical props that developers often miss.

The priority Prop

This is the single most common Next.js LCP mistake. If your above-fold image does not have priority, Next.js lazy-loads it by default. That means the browser waits until it parses your component tree before it even starts downloading your hero image. Your LCP tanks.

// Wrong: hero image gets lazy-loaded, LCP suffers
<Image
  src="/hero.webp"
  alt="Hero image"
  width={1200}
  height={630}
/>

// Right: browser fetches this immediately
<Image
  src="/hero.webp"
  alt="Hero image"
  width={1200}
  height={630}
  priority
/>

Rule: any image visible above the fold on first load should have priority. Usually that is one image per page, two at most. Do not add priority to everything or you lose the benefit.

The sizes Prop for Responsive Images

Without sizes, Next.js generates a default srcset but cannot optimize for different viewport sizes. With sizes, it can serve appropriately sized images for phones versus desktops:

<Image
  src="/hero.webp"
  alt="Hero"
  fill
  priority
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
/>

This tells Next.js that on mobile the image fills the full viewport width, on tablet it fills 80%, and on desktop it is 1200px. Next.js uses this to generate an appropriate srcset. Mobile users download a much smaller image.

blur Placeholder for CLS Prevention

Images loading after the page renders can cause layout shift. The blur placeholder reserves space immediately with a low-res placeholder, preventing CLS:

// With local images: automatic blurDataURL
import heroImg from '../public/hero.webp';

<Image
  src={heroImg}
  alt="Hero"
  placeholder="blur"  // Next.js generates blurDataURL automatically
  priority
/>

// With remote images: provide blurDataURL manually
<Image
  src="https://cdn.example.com/hero.webp"
  alt="Hero"
  width={1200}
  height={630}
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
/>

next/font: Stop FOUT and CLS From Fonts

If you are still using this in your app:

<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" />

Switch to next/font immediately. It self-hosts the font, eliminates the external request, and applies font-display: optional by default (which prevents CLS). The difference in CLS score can be dramatic.

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',       // Or 'optional' to prevent FOUT entirely
  variable: '--font-inter',
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}

What this does: Next.js downloads the font at build time, hosts it on your own domain, inlines the font-face CSS so there is no extra request, and applies the display strategy you choose. No flash, no shift, no external dependency at runtime.

React Server Components vs Client Components: CWV Impact

Here is the thing about 'use client' that many developers do not fully understand. When you mark a component as a client component, it does not just make that component interactive. It also sends all of its JavaScript to the browser. Including the JavaScript for all of its imports.

// This ships React runtime + lodash + chart.js + your component to the browser
// even if the user never interacts with it
'use client';
import _ from 'lodash';
import { BarChart } from 'chart.js';

export function StatsSection({ data }) {
  return <BarChart data={_.groupBy(data, 'category')} />;
}

If that component is above the fold, all that JavaScript parsing competes with rendering your LCP element. INP also suffers because a larger JS bundle means more hydration work when the user first interacts with the page.

The fix: keep client components as leaves, not roots. Move state down to the smallest component that needs it. Let parent components be server components that render fast HTML with no JavaScript cost.

Code Splitting for INP: next/dynamic in Practice

Heavy components like rich text editors, chart libraries, or complex modals should not be in your initial JavaScript bundle. Use next/dynamic to load them only when needed:

import dynamic from 'next/dynamic';

// Only loads when the component is rendered
const RichTextEditor = dynamic(
  () => import('./RichTextEditor'),
  {
    loading: () => <div style={{height: '300px', background: '#f1f5f9'}} />,
    ssr: false,  // Editor only runs in browser
  }
);

export function BlogPost({ post }) {
  const [editing, setEditing] = useState(false);

  return (
    <div>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      {editing && <RichTextEditor value={post.content} />}
    </div>
  );
}

That loading prop is important for CLS. It reserves space with a fixed-height placeholder while the component loads. Without it, the editor loads and shifts everything below it down.

App Router vs Pages Router: CWV Reality Check

If you are deciding between App Router and Pages Router, here is the honest CWV comparison:

Factor App Router Pages Router
LCP potential Better (RSC reduces JS) Good but more JS to client
CLS risk Higher (streaming can shift) Lower if done right
INP Better (less client JS) Depends on hydration
Learning curve Steep Familiar patterns
CWV footguns More (streaming CLS, excessive 'use client') Fewer but heavier hydration

App Router has better CWV ceiling if used correctly. But it also has more ways to accidentally hurt your CWV if you over-use client components or mismanage Suspense boundaries. Pages Router is more predictable but harder to get excellent CWV on complex apps.

Quick Next.js CWV Checklist

Next.js Performance Checklist

FAQ

Does Turbopack improve Core Web Vitals?

Turbopack improves build speed and hot module replacement, not runtime CWV. It makes development faster and can produce slightly smaller bundles in some cases, but it does not directly change what your users experience. Your CWV are determined by what ships to the browser, not how fast it was compiled.

Should I use Vercel for better CWV?

Vercel's edge network genuinely helps TTFB for users far from your server. If you are self-hosting in one region, users in other continents see TTFB of 500ms or more. Vercel's CDN reduces that to under 100ms globally. That directly improves LCP. It is not magic, it is just a good CDN. Cloudflare does the same thing for less money if you self-host.

My Lighthouse score is 95 but CrUX field data is failing. What is wrong?

This is the lab vs field gap in action. Your Lighthouse test runs under controlled conditions. Real users have slower devices, slower connections, more browser extensions, more tabs open. Check which specific metric is failing in PSI field data, then profile that metric on a throttled mobile device in DevTools.

Still Getting Red Scores?

Run a free audit and get a punch-list of exactly what to fix. No account needed.

Run Free Audit →

Still Getting Red Scores?

Run your site through VitalsFixer. Free audit in 30 seconds, no account needed.

Analyze My Site Free →

Want an Expert to Handle It?

Real engineers, 48-hour turnaround, money back if scores don't improve.

View Expert Fix →