LCP optimization reference
What is LCP?
Largest Contentful Paint (LCP) measures when the largest content element in the viewport becomes visible. This is typically:
- An
<img>element - An
<image>element inside<svg> - A
<video>element with poster image - An element with a background image via
url() - A block-level element containing text nodes
LCP timeline
[ Server Response ][ Resource Load ][ Render ]
TTFB Download Paint
└─────────────────────────────────────┘
LCP TimeDetailed optimizations
1. Server response time (TTFB)
Target: < 800ms
Causes:
- Slow server/database queries
- No CDN/edge caching
- Inefficient backend code
- Cold starts (serverless)
Solutions:
// Use edge functions for dynamic content
// Vercel example
export const config = { runtime: 'edge' };
// Use stale-while-revalidate caching
// Cache-Control header
res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate=300');2. Resource load time
For images:
<!-- Preload LCP image -->
<link rel="preload" as="image" href="/hero.webp"
imagesrcset="/hero-400.webp 400w, /hero-800.webp 800w"
imagesizes="100vw"
fetchpriority="high">
<!-- Modern format with fallback -->
<picture>
<source srcset="/hero.avif" type="image/avif">
<source srcset="/hero.webp" type="image/webp">
<img src="/hero.jpg" width="1200" height="600"
fetchpriority="high" alt="Hero">
</picture>For text (web fonts):
@font-face {
font-family: 'Heading';
src: url('/fonts/heading.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
}3. Render blocking resources
Critical CSS pattern:
<head>
<!-- Inline critical CSS -->
<style>
/* Only above-fold styles, < 14KB */
.hero { /* ... */ }
.nav { /* ... */ }
</style>
<!-- Defer non-critical CSS -->
<link rel="preload" href="/styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
</head>Defer JavaScript:
<!-- ❌ Blocks parsing -->
<script src="/app.js"></script>
<!-- ✅ Deferred (runs after HTML parsed) -->
<script defer src="/app.js"></script>
<!-- ✅ Module (deferred by default) -->
<script type="module" src="/app.mjs"></script>4. Client-side rendering
Problem: Content not in initial HTML.
Solutions:
Server-side rendering (SSR):
// Next.js
export async function getServerSideProps() {
const data = await fetchHeroContent();
return { props: { hero: data } };
}Static site generation (SSG):
// Next.js
export async function getStaticProps() {
const data = await fetchHeroContent();
return { props: { hero: data }, revalidate: 3600 };
}Streaming SSR:
// React 18+
import { Suspense } from 'react';
function Page() {
return (
<Suspense fallback={<HeroSkeleton />}>
<Hero />
</Suspense>
);
}Framework-specific tips
Next.js
import Image from 'next/image';
// LCP image with priority
<Image
src="/hero.jpg"
priority
fill
sizes="100vw"
alt="Hero"
/>Nuxt
<NuxtImg
src="/hero.jpg"
preload
loading="eager"
sizes="100vw"
/>Astro
---
import { Image } from 'astro:assets';
import hero from '../assets/hero.jpg';
---
<Image
src={hero}
loading="eager"
decoding="sync"
alt="Hero"
/>Debugging LCP
// Identify LCP element
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', {
element: lastEntry.element,
time: lastEntry.startTime,
size: lastEntry.size,
url: lastEntry.url,
renderTime: lastEntry.renderTime,
loadTime: lastEntry.loadTime
});
}).observe({ type: 'largest-contentful-paint', buffered: true });Common issues
| Issue | Impact | Fix |
|---|---|---|
| No preload for LCP image | +500-1000ms | Add <link rel="preload"> |
| Large unoptimized image | +300-800ms | Compress, use WebP/AVIF |
| Render-blocking CSS | +200-500ms | Inline critical CSS |
| Slow TTFB | +300-2000ms | CDN, edge caching |
| Client-rendered content | +500-2000ms | SSR/SSG |