Largest Contentful Paint, explained: it's the metric that separates sites visitors actually see quickly from those that vanish into loading spinners. On most projects I've shipped, LCP is the first metric to cross the performance budget threshold—because every team prioritizes above-the-fold images and text, but almost nobody sets it up correctly from the start. If you want Core Web Vitals compliance, LCP is non-negotiable.
What is Largest Contentful Paint?
Largest Contentful Paint (LCP) measures when the largest visible element on the viewport first renders. That's your hero image, your main heading, your primary call-to-action card—whichever is physically biggest. The browser fires the LCP event once, at the moment that element becomes visible to the user. Google marks anything under 2.5 seconds as "good," 2.5 to 4 seconds as "needs improvement," and over 4 seconds as "poor."
Why does this matter? Because it's the visual signal that tells humans your page is loading. A blank screen for 5 seconds kills conversion. A hero image at 1.8 seconds keeps them engaged.
LCP Breakdown — The Four Steps to Understand Where Time Goes
Every LCP delay falls into one of four buckets. Understanding these helps you pinpoint exactly where your site bleeds milliseconds.
Time to First Byte (TTFB)
Before the browser can paint anything, your server must send the first byte of the HTML document. This includes network latency, server processing, and any backend work. On a slow origin server or distant region, TTFB eats 500–2000ms before rendering even begins.
I worked on a SaaS platform in 2023 where the dashboard took 3.2 seconds for LCP. Profiling revealed TTFB was 1.8 seconds—the backend was querying 12 related tables sequentially. We moved to a single aggregated query and cached the result. LCP dropped to 2.1 seconds. Server speed is not optional.
Resource Load Delay
After the HTML arrives, the browser parses it, discovers your hero image (or other LCP candidates), and then waits to start downloading it. This delay grows when you lazy-load images, when you load images only after JavaScript runs, or when you load them in a low-priority network queue.
Lazy-loading a hero is a classic foot-gun. I've seen dozens of projects use loading="lazy" on the above-the-fold image. The image never starts downloading until the browser scrolls it into view—but the viewport is already showing it, so the browser prioritizes other things first. LCP explodes to 4+ seconds.
Resource Load Time
Once the browser starts fetching your LCP image, it must download it over the network. A 200KB unoptimized JPEG takes longer than a 30KB WebP. A render-blocking stylesheet or font also delays the resource load timeline.
On a ride-hailing platform I built, the hero image was 450KB. Converting to WebP and adding responsive srcsets cut it to 80KB. Load time on 4G dropped from 1.2 seconds to 180ms. Network optimization is measurable.
Element Render Delay
Even after the resource downloads, the browser must decode and render it. For images, this is usually fast. For text rendered by JavaScript frameworks (React, Vue, Angular), the render delay is brutal. The DOM doesn't exist until JavaScript hydrates the component tree.
Client-side rendered hero text is a silent killer. On a travel booking site, the main heading was rendered in React. The component tree took 2.1 seconds to hydrate, then 300ms more to layout and paint the text. Total LCP: 2.4 seconds. We server-rendered the heading as static HTML, moved hydration to secondary content, and LCP fell to 0.9 seconds.
Find the LCP Element — Chrome DevTools Performance Tab
Guessing which element is the LCP is a waste of time. The browser knows exactly which element triggered the LCP event.
Open Chrome DevTools. Go to Performance. Hit record, navigate to your page, stop recording. Scroll down to the "Timings" track. Look for "Largest Contentful Paint." Click it. DevTools highlights the exact element in the rendered viewport and shows you the element node in the DOM tree on the right.
Most of the time, it's your hero image. Sometimes it's a large heading or a block of text that loads after the image. Occasionally it's a video thumbnail or an advertisement. Knowing this is step one. Everything else flows from it.
| Element Type | Common LCP Cause | Typical Fix |
|---|---|---|
| Hero image | Not preloaded, lazy-loaded, or unoptimized | Preload link, remove lazy-loading, optimize image |
| Heading/text | Rendered by JavaScript framework | Server-render the element, inline CSS |
| Video thumbnail | Poster image is large and slow | Optimize poster, use WebP, responsive sizes |
| Lazy-loaded component | JavaScript initializes off viewport first | Render component on server, prioritize in JS bundle |
Load It First — Preload and Prioritize the LCP Resource
Once you know your LCP element, make the browser download it before anything else. This is not optional.
If your LCP is an image, add a preload link in the head:
<head>
<link
rel="preload"
as="image"
href="/images/hero.webp"
type="image/webp"
fetchpriority="high"
/>
</head>The fetchpriority="high" attribute tells the browser to bump this resource above stylesheets, fonts, and scripts in the download queue. Without it, the browser may still delay the image.
For responsive images, include a srcset in the preload:
<link
rel="preload"
as="image"
href="/images/hero-1200.webp"
imagesrcset="/images/hero-640.webp 640w, /images/hero-1200.webp 1200w, /images/hero-1920.webp 1920w"
imagesizes="(max-width: 600px) 100vw, (max-width: 1200px) 80vw, 1200px"
fetchpriority="high"
/>Never lazy-load your LCP image. This is a hard rule. I've seen engineers argue that loading="lazy" is "always good practice." It's not. Lazy-loading defers image download until the browser scrolls the image into view. For above-the-fold images, this is backwards—the image is already in view, so the browser never downloads it early.
If you're worried about performance on slower connections, use a preload with a conditional. On an e-commerce checkout flow, I load the hero preload only on desktop with a media query:
<link
rel="preload"
as="image"
href="/images/hero-desktop.webp"
media="(min-width: 768px)"
fetchpriority="high"
/>On mobile, the image is still <img> tag—the browser downloads it early anyway because it's visible—but without the extra preload link overhead.
Server-Render It — Avoid Client-Side Rendering for LCP Elements
This is where most frameworks trip up teams. When your LCP element is rendered by JavaScript, the browser waits for three things: the HTML to arrive, the JavaScript bundle to arrive, and the JavaScript to execute and paint the DOM. That's a cumulative delay.
A Next.js site I worked on had a hero component that fetched a product title from an API on mount:
// pages/product/[id].js — CLIENT-SIDE RENDER
export default function ProductPage({ id }) {
const [title, setTitle] = useState(null);
useEffect(() => {
fetch(`/api/product/${id}`)
.then(res => res.json())
.then(data => setTitle(data.title));
}, [id]);
return <h1>{title || 'Loading...'}</h1>;
}
// LCP: ~2.5 seconds on 4G (JS parse + API call + render)We server-rendered the component instead:
// pages/product/[id].js — SERVER-SIDE RENDER
export async function getServerSideProps({ params }) {
const product = await fetch(`https://api.example.com/product/${params.id}`);
const data = await product.json();
return { props: { title: data.title } };
}
export default function ProductPage({ title }) {
return <h1>{title}</h1>; // Static HTML from the start
}
// LCP: ~0.9 seconds on 4G (no JS wait, no API call in browser)The heading was now in the initial HTML. The browser painted it within the HTML parse phase. LCP improved by 65%.
If you must use client-side rendering, inline the critical data in a script tag:
<!-- HTML template -->
<script>
window.__INITIAL_STATE__ = { title: 'Product Name' };
</script>
<div id="app"></div>
<script src="bundle.js" fetchpriority="high"></script>The JavaScript still runs, but it reads from memory instead of fetching. Not as fast as full server render, but better than waiting for an API call.
Common Mistakes
- Lazy-loading above-the-fold images. Remove
loading="lazy"from your hero and any image in the viewport. Use it only for below-fold images. - Not using preload on the LCP image. Preload is not optional. It changes the network priority queue. Without it, the browser may deprioritize your hero behind stylesheets or fonts.
- Render-blocking resources before the LCP element. If you load a full CSS framework before your hero image, the image sits in the queue. Inline critical CSS or defer non-critical stylesheets.
- Client-rendering text that should be HTML. Headings, labels, and static text do not need JavaScript. Render them on the server and ship them in the HTML.
- Serving oversized images. A 5MB PNG is not acceptable for any hero. Compress, convert to WebP, and use responsive sizes. DevTools Lighthouse will flag this immediately.
- Third-party scripts delaying LCP. Analytics, chat widgets, and ads often load synchronously. Defer them or load them asynchronously after LCP fires.
Key Takeaways
- Largest Contentful Paint measures when the biggest visible element renders. Good is under 2.5 seconds, poor is over 4 seconds.
- LCP delay breaks into four categories: TTFB, resource load delay, resource load time, and element render delay. Identify which one hurts you most using Chrome DevTools.
- Find your LCP element in the Performance tab. It's usually the hero image or main heading.
- Preload the LCP resource with
rel="preload"andfetchpriority="high". Never lazy-load above-the-fold images. - Server-render your LCP element to avoid waiting for JavaScript. Client-rendering adds at least 1–2 seconds.
- Optimize the image size (WebP, responsive srcsets) and minimize TTFB (faster servers, closer regions). Every millisecond counts.






Comments
Loading comments…