Fix LCP in Next.js
Largest Contentful Paint (LCP) is the most common Core Web Vital issue in Next.js applications. Despite Next.js providing built-in optimization features, many developers miss critical configuration steps that leave their LCP scores in the "needs improvement" or "poor" range.
This guide walks through the five most impactful fixes for LCP in Next.js, with real code examples and before/after performance comparisons. These fixes apply to both the App Router and Pages Router.
Expected results
Following all five steps typically produces these improvements:
Before
4.2s
LCP score (Poor) — unoptimized Next.js defaults with standard img tags and no preloading
After
1.4s
LCP score (Good) — optimized with priority images, SSG, self-hosted fonts, and RSC
Step-by-step fix
Use next/image with the priority prop
The next/image component automatically optimizes images (WebP conversion, responsive sizing, lazy loading). But for the LCP image, you must add the priority prop — this enables preloading and disables lazy loading for that specific image.
priority prop on the LCP image. Without it, next/image uses loading="lazy" by default, which delays loading and directly hurts LCP.
// ❌ Standard img tag — no optimization
export default function Hero() {
return (
<img
src="/hero-image.jpg"
alt="Product showcase"
style={{ width: '100%', height: 'auto' }}
/>
);
}
// ✅ next/image with priority for LCP image
import Image from 'next/image';
export default function Hero() {
return (
<Image
src="/hero-image.jpg"
alt="Product showcase"
width={1200}
height={630}
priority // Preloads the image
sizes="100vw" // Full viewport width
quality={85} // Balance quality vs size
/>
);
}
Switch to Static Generation or ISR
Pages using getServerSideProps generate HTML on every request, adding server processing time to TTFB. Switch to getStaticProps (Pages Router) or default static rendering (App Router) with ISR for pages that don't need real-time data.
// app/blog/[slug]/page.tsx
// Revalidate every 60 seconds — ISR
export const revalidate = 60;
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<Image
src={post.coverImage}
alt={post.coverAlt}
width={1200}
height={630}
priority
/>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Optimize font loading with next/font
Web fonts are a hidden LCP bottleneck. The browser blocks text rendering until fonts load (FOIT) or shows fallback fonts that shift layout (FOUT). The next/font module self-hosts fonts, eliminating the round-trip to Google Fonts and enabling automatic subsetting.
import { Inter, JetBrains_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-body',
});
const jetbrains = JetBrains_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-mono',
});
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={`${inter.variable} ${jetbrains.variable}`}>
<body>{children}</body>
</html>
);
}
Reduce JavaScript bundle size
Large JavaScript bundles block the main thread and delay rendering. Use next/dynamic for below-the-fold components, analyze your bundle with @next/bundle-analyzer, and aggressively tree-shake unused code. A common win: replacing moment.js (300KB) with date-fns (12KB) or dayjs (2KB).
import dynamic from 'next/dynamic';
// Heavy components loaded only when needed
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <div className="chart-skeleton" />,
ssr: false, // Skip SSR for client-only components
});
const CommentSection = dynamic(
() => import('@/components/Comments'),
{ ssr: false }
);
export default function Dashboard() {
return (
<main>
{/* LCP content loads immediately */}
<h1>Dashboard</h1>
<MetricsOverview />
{/* Below-fold components lazy-loaded */}
<HeavyChart />
<CommentSection />
</main>
);
}
Use React Server Components
If you're on the App Router, take advantage of React Server Components (RSC). Server Components don't send any JavaScript to the browser — they render HTML on the server and stream it to the client. This dramatically reduces bundle size and eliminates hydration delay for non-interactive content.
// This is a Server Component by default — no 'use client'
// It sends ZERO JavaScript to the browser
import Image from 'next/image';
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 3600 }, // Cache for 1 hour
});
return res.json();
}
export default async function ProductPage({
params,
}: {
params: { id: string }
}) {
const product = await getProduct(params.id);
return (
<article>
<Image
src={product.image}
alt={product.name}
width={800}
height={600}
priority
/>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Only interactive parts need 'use client' */}
<AddToCartButton productId={product.id} />
</article>
);
}
Quick checklist
-
LCP image uses
next/imagewithpriorityprop - Page uses SSG or ISR (not SSR) where possible
-
Fonts self-hosted with
next/fontanddisplay: swap -
Below-fold components use
next/dynamic - Non-interactive content uses Server Components (App Router)
-
Third-party scripts use
next/scriptwith lazy strategy -
Bundle analyzed with
@next/bundle-analyzer
Frequently asked questions
A well-optimized Next.js site using SSG or ISR should achieve LCP under 1.8 seconds. With SSR, expect 2.0-2.5 seconds. If you're seeing LCP above 3 seconds, there are likely optimization opportunities in image handling, font loading, or JavaScript bundle size.
Not automatically. You must add the priority prop to the LCP image. Without it, next/image uses lazy loading by default, which actually hurts LCP. The component provides optimization tools (WebP conversion, responsive sizing, blur placeholders), but the priority prop is required to activate preloading for above-the-fold images.
Generally yes. The App Router enables React Server Components (less client JS), supports streaming SSR, and has better built-in optimizations. That said, a well-optimized Pages Router app can also achieve excellent LCP scores. The App Router advantage is most pronounced on JavaScript-heavy pages.
Third-party scripts (analytics, chat widgets, A/B testing tools) can add 500ms-2s to your LCP by competing for bandwidth and blocking the main thread. Use next/script with strategy="lazyOnload" or strategy="afterInteractive" to defer them. For analytics that must fire early, use the Web Vitals reporting callback instead.
SSG almost always wins on LCP because pre-built HTML serves from CDN edge nodes with near-zero TTFB. SSR adds server processing time per request. Use ISR (Incremental Static Regeneration) to combine SSG's performance with SSR's freshness — pages revalidate in the background while serving cached HTML to users.
Google rates LCP as 'good' when it is under 2.5 seconds at the 75th percentile. For Nextjs applications specifically, aim for under 2.0 seconds. Measure with field data from Chrome User Experience Report (CrUX) through PageSpeed Insights, as lab tests may not reflect real-user experience with third-party scripts and varying network conditions.
Related resources
Complete LCP Guide
The comprehensive guide to understanding and optimizing Largest Contentful Paint.
AnalysisState of Web Performance in 2026
How frameworks compare on Core Web Vitals across 10M URLs.
BenchmarkNext.js vs Remix 2026 Performance
Head-to-head LCP, INP, and TTFB benchmarks for Next.js 15.2 and Remix 3.1.
Continue learning
Complete LCP Guide
Deep dive into LCP -- thresholds, measurement, and optimization strategies.
FixFix CLS in Next.js
Related performance optimization for the same framework.
FixFix INP in Next.js
Related performance optimization for the same framework.
ToolCWV Score Explainer
Enter your scores for personalized fix recommendations.