Core Web Vitals for Next.js App Router: The Complete Fix Guide
LCP, CLS, and INP failing on your Next.js app? This guide covers the exact causes and fixes for each metric, with code examples for App Router.
Core Web Vitals directly affect Google rankings. A poor score doesn't just mean a slow site — it means lower organic traffic. Here's a systematic guide to diagnosing and fixing CWV issues on Next.js App Router projects.
The three metrics (and what actually causes them to fail)
LCP (Largest Contentful Paint) — time until the largest visible element is painted. Target: under 2.5s.
Most common causes on Next.js:
- Hero image not using
next/imagewithpriority - LCP image being lazy-loaded
- Slow server response time (TTFB)
- Render-blocking fonts
CLS (Cumulative Layout Shift) — unexpected layout movement. Target: under 0.1.
Most common causes:
- Images without
width/height(oraspect-ratio) - Fonts causing FOUT (Flash of Unstyled Text)
- Dynamic content injected above existing content
- Ad slots without reserved space
INP (Interaction to Next Paint) — responsiveness to user interaction. Target: under 200ms. Replaced FID in March 2024.
Most common causes:
- Long Tasks blocking the main thread
- Heavy client-side JavaScript on interaction
- Unoptimised re-renders in React
Fixing LCP
1. Use next/image with priority for your hero image
import Image from "next/image";
export default function Hero() {
return (
<Image
src="/hero.webp"
alt="Hero image"
width={1200}
height={630}
priority // disables lazy loading, adds fetchpriority="high"
sizes="100vw"
/>
);
}
priority adds fetchpriority="high" and removes the loading="lazy" attribute. Your LCP image must have this. Without it, the browser treats it as a low-priority resource.
2. Preload the LCP image
If your LCP element is a CSS background image (not an <img>), next/image won't help. Add a preload hint in your layout:
// app/layout.tsx
import { Metadata } from "next";
export default function RootLayout({ children }) {
return (
<html>
<head>
<link
rel="preload"
as="image"
href="/hero.webp"
imageSrcSet="/hero-480.webp 480w, /hero-1200.webp 1200w"
imageSizes="100vw"
/>
</head>
<body>{children}</body>
</html>
);
}
3. Reduce Time to First Byte (TTFB)
TTFB > 600ms will push LCP past the 2.5s threshold regardless of other optimisations.
Diagnose with: curl -w "%{time_starttransfer}\n" -o /dev/null -s https://yoursite.com
Common fixes:
- Enable ISR or static generation for high-traffic pages (avoid
dynamic = "force-dynamic"unless necessary) - Move to an edge-compatible region closer to your users
- Add
Cache-Control: s-maxage=3600, stale-while-revalidate=86400headers for static content
4. Self-host your fonts
Google Fonts adds a DNS lookup and connection to fonts.googleapis.com. Use next/font/google instead — it downloads the font at build time and self-hosts it:
import { Geist } from "next/font/google";
const geist = Geist({
subsets: ["latin"],
variable: "--font-sans",
display: "swap", // prevents FOIT (flash of invisible text)
});
next/font automatically adds font-display: swap and removes the external request.
Fixing CLS
1. Always specify image dimensions
// Wrong — CLS
<img src="/photo.jpg" alt="Photo" />
// Right — no CLS
<Image src="/photo.jpg" alt="Photo" width={800} height={600} />
// Or with aspect-ratio if dimensions vary
<div style={{ aspectRatio: "4/3", position: "relative" }}>
<Image src="/photo.jpg" alt="Photo" fill sizes="800px" />
</div>
2. Reserve space for dynamic content
If you're injecting content after load (cookie banners, notification bars, deferred widgets):
.cookie-banner {
/* Reserve space even before content loads */
min-height: 64px;
position: fixed;
bottom: 0;
/* or use position: fixed to avoid layout shift entirely */
}
Fixed or sticky positioned elements don't cause CLS because they don't affect document flow.
3. Font FOUT causes CLS
When a fallback font has different metrics to the web font, swapping causes layout shift. Use next/font's adjustFontFallback option or manually define font metrics:
const instrumentSerif = Instrument_Serif({
weight: ["400"],
subsets: ["latin"],
variable: "--font-serif",
display: "swap",
adjustFontFallback: true, // auto-generates fallback with matching metrics
});
Fixing INP
INP replaced FID in March 2024. FID only measured the delay to start processing an interaction; INP measures the full time from interaction to the next frame painted.
1. Break up Long Tasks
Any JavaScript task over 50ms blocks the main thread and delays interaction response. Use the scheduler to yield between chunks:
async function processLargeDataset(items: Item[]) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// Yield to browser every 50 items
if (i % 50 === 0) {
await scheduler.yield(); // Chrome 115+
// Fallback: await new Promise(r => setTimeout(r, 0));
}
}
}
2. Defer non-critical JavaScript
Next.js App Router server components help here — components that don't need interactivity don't ship JavaScript. But for third-party scripts:
import Script from "next/script";
// strategy="lazyOnload" waits until browser is idle
<Script src="https://analytics.example.com/script.js" strategy="lazyOnload" />
// strategy="afterInteractive" loads after hydration
<Script src="https://tag-manager.example.com/gtm.js" strategy="afterInteractive" />
Never use strategy="beforeInteractive" for analytics or tag managers.
3. Optimise React re-renders triggered by interactions
INP failures often come from a click handler that triggers a large component tree re-render. Profile with React DevTools Profiler — look for components re-rendering that shouldn't be.
Common fixes:
useMemofor expensive computed valuesuseCallbackfor stable function references passed as propsReact.memofor pure components that receive the same props on re-render- Move state down to the smallest subtree that needs it
The measurement workflow
- Baseline: Run PageSpeed Insights on your 3 most important pages. Record LCP, CLS, INP.
- Lab testing:
npx unlighthousefor a site-wide Lighthouse audit — scans every page automatically. - Field data: Check Search Console → Core Web Vitals report. Lab scores and field scores differ — field data is what Google uses for ranking.
- After fixes: Deploy, wait 28 days (Google's rolling average window), re-check Search Console.
Quick wins by impact
Fix these first — they usually move the needle most:
- Add
priorityto your LCP image (next/image) — 15–30% LCP improvement - Switch from
<img>tonext/imagewith explicit dimensions — eliminates most CLS - Move to
next/fontfrom manual<link>Google Fonts — removes external request, fixes FOUT - Set
strategy="lazyOnload"on all third-party scripts — INP improvement + LCP improvement - Check
dynamic = "force-dynamic"pages — convert to ISR where possible — TTFB improvement
If you've fixed the obvious issues and CWV still isn't where you need it, the remaining gains usually come from TTFB and Long Tasks — both require profiling with real traffic data. We do full technical SEO engagements that include CWV diagnosis and implementation.
Work with us
Need help applying this to your stack?
Free 30-min strategy call. We'll scope your problem and tell you honestly what the fix looks like.