LCP - Largest Contentful Paint
Deep dive into optimizing Largest Contentful Paint for faster perceived load times
Largest Contentful Paint (LCP) measures how long it takes for the largest content element in the viewport to become visible. It's the most impactful Core Web Vital for perceived performance.
Understanding LCP
Score Thresholds
| Score | Rating | User Perception |
|---|---|---|
| ≤ 2.5s | Good | Page feels fast |
| 2.5s - 4s | Needs Improvement | Noticeable delay |
| > 4s | Poor | Frustrating wait |
What Elements Count as LCP?
The LCP element can be:
<img>elements<image>inside SVG<video>poster images- Background images via
url() - Block-level text elements (
<p>,<h1>, etc.)
The LCP element often changes as the page loads. The final LCP is measured when the largest element finishes rendering.
LCP Breakdown
LCP consists of four sub-parts:
| Component | Target | Description |
|---|---|---|
| TTFB | < 800ms | Server response time |
| Resource Load Delay | < 10% of LCP | Time before resource starts loading |
| Resource Load Time | Minimize | Time to download the resource |
| Render Delay | < 10% of LCP | Time from load to paint |
Common LCP Issues in Next.js
1. Non-Priority LCP Images
// ❌ Bad: LCP image loads lazily by default
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} />
// ✅ Good: Priority loading for LCP image
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
/>2. Slow Server Response (TTFB)
// ❌ Bad: Fetching data on every request
export async function getServerSideProps() {
const data = await fetchFromSlowAPI();
return { props: { data } };
}
// ✅ Good: Use ISR for better TTFB
export async function getStaticProps() {
const data = await fetchFromSlowAPI();
return {
props: { data },
revalidate: 60
};
}3. Render-Blocking Resources
// ❌ Bad: Loading heavy component synchronously
import HeavyChart from '@/components/HeavyChart';
// ✅ Good: Dynamic import for non-LCP content
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false
});4. Large Unoptimized Images
// ❌ Bad: Full-size image without optimization
<img src="/hero-4000x2000.jpg" alt="Hero" />
// ✅ Good: Responsive images with Next.js
<Image
src="/hero.jpg"
alt="Hero"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 1200px"
priority
/>Optimization Strategies
Preload Critical Resources
import Head from 'next/head';
export default function Layout({ children }) {
return (
<>
<Head>
{/* Preload LCP image */}
<link
rel="preload"
as="image"
href="/hero.webp"
type="image/webp"
/>
{/* Preconnect to image CDN */}
<link
rel="preconnect"
href="https://images.example.com"
/>
</Head>
{children}
</>
);
}Optimize Hero Section
import Image from 'next/image';
export function Hero() {
return (
<section className="relative h-[600px]">
{/* LCP image with all optimizations */}
<Image
src="/hero.jpg"
alt="Hero background"
fill
priority
quality={85}
sizes="100vw"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
className="object-cover"
/>
{/* Text content renders immediately */}
<div className="relative z-10 p-8">
<h1 className="text-5xl font-bold">Welcome</h1>
</div>
</section>
);
}Server-Side Rendering Optimization
// Use streaming for faster initial paint
import { Suspense } from 'react';
export default function Page() {
return (
<>
{/* Critical content renders first */}
<Hero />
{/* Non-critical content streams in */}
<Suspense fallback={<ProductsSkeleton />}>
<ProductList />
</Suspense>
</>
);
}Font Loading Strategy
// Fonts can be LCP elements if they're the largest text
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
preload: true,
fallback: ['system-ui', 'arial'],
});
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
);
}Diagnosing LCP Issues
Using Chrome DevTools
Open Performance panel Press F12 → Performance tab → Record a page load
Find LCP marker Look for the "LCP" label in the Timings row
Identify the element Hover over LCP to see which element triggered it
Analyze the waterfall Check when the LCP resource started/finished loading
Programmatic Measurement
import { onLCP, type LCPMetric } from 'web-vitals';
export function measureLCP() {
onLCP((metric: LCPMetric) => {
console.log('LCP:', metric.value, 'ms');
console.log('LCP Element:', metric.entries[0]?.element);
// Send to analytics
sendToAnalytics({
name: 'LCP',
value: metric.value,
rating: metric.rating,
element: metric.entries[0]?.element?.tagName,
});
});
}Advanced Techniques
Critical CSS Inlining
module.exports = {
experimental: {
optimizeCss: true, // Enables CSS optimization
},
};Image CDN Configuration
module.exports = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.ts',
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};Predictive Prefetching
// Prefetch likely next pages
import { useRouter } from 'next/router';
import { useEffect } from 'react';
function usePrefetchOnHover(href: string) {
const router = useRouter();
return {
onMouseEnter: () => router.prefetch(href),
onFocus: () => router.prefetch(href),
};
}LCP Optimization Checklist
Images
Add priority to LCP images
The most critical optimization - tells Next.js to preload the hero image.
Use WebP/AVIF formats Modern formats are 25-50% smaller than JPEG/PNG.
Implement responsive sizes
Ensure correct image size loads for each viewport.
Add blur placeholder Improves perceived performance while images load.
Preload critical images
Use <link rel="preload"> for above-the-fold images.
Server Response
Use static generation when possible Pre-rendered pages have the fastest TTFB.
Implement ISR for dynamic content Best of both worlds: static speed with dynamic updates.
Add CDN caching headers Cache static assets and responses at the edge.
Optimize database queries Slow queries directly impact server response time.
Use edge functions for personalization Move personalization logic closer to users.
Rendering
Minimize render-blocking resources Defer or inline CSS/JS that blocks first paint.
Inline critical CSS Include above-the-fold styles directly in the HTML.
Defer non-critical JavaScript Load analytics and widgets after main content.
Use streaming SSR with Suspense Start sending HTML before all data is ready.
Reduce DOM size Fewer nodes means faster rendering.
Real-World Impact
| Optimization | LCP Improvement |
|---|---|
Adding priority to hero image | -300ms to -800ms |
| SSG instead of SSR | -200ms to -500ms |
| Image CDN (Cloudflare/Vercel) | -100ms to -400ms |
| Font optimization | -50ms to -200ms |
| Critical CSS inlining | -50ms to -150ms |