Largest Contentful Paint (LCP) — what slows it down and how to fix it
LCP = when the largest visible content element finishes painting (target < 2.5s). Slowed by: slow server (TTFB), render-blocking CSS/JS, late-discovered hero image, large image bytes, slow networks, client-side rendering. Fixes: fast server/CDN, inline critical CSS, defer non-critical JS, `<link rel='preload'>` the hero, modern image formats + responsive sizes, SSR/SSG.
LCP measures when the largest visible content element finishes painting — usually the hero image, a large headline, or a video poster. Google's target is < 2.5s at the 75th percentile.
What can be the LCP element
- An
<img>(most common). - A
<video>poster. - A background image declared in CSS.
- A block-level text node large enough to be the biggest visible element.
The browser picks dynamically — the LCP can change as elements enter the viewport. The final LCP is the largest at the moment user input occurs (or page hides).
What slows it down
1. Slow TTFB (server response)
If the server takes 1s to respond, your LCP can't be under 1s. Fix at the server: CDN edge caching, faster backend, HTTP/2 or 3.
2. Render-blocking CSS
The browser won't paint until all stylesheets are parsed. A 500KB CSS bundle delays everything.
3. Render-blocking JS
Synchronous <script> in the head blocks HTML parsing → delays everything downstream.
4. Late-discovered hero image
If the hero is set via CSS background-image or injected by JS, the browser doesn't see it during preload scanning and starts the download late.
5. Large image bytes
The hero is a 2MB unoptimized PNG. Download time dominates LCP on mobile networks.
6. Client-side rendering
CSR means HTML arrives empty; LCP can't happen until the JS bundle loads, parses, executes, fetches data, and renders. Easy to slip to 4–5s on mobile.
7. Web fonts
If headline text is the LCP element, a slow font with font-display: block delays the paint.
How to fix it
Fix the LCP element specifically
<link rel="preload" as="image" fetchpriority="high" href="hero.jpg">in the head — starts the hero download immediately.fetchpriority="high"attribute on the<img>itself.- Avoid CSS background-image for the LCP; use a real
<img>so the preload scanner sees it. - Modern formats — WebP/AVIF; ~30–50% smaller than JPEG.
- Responsive sizes —
srcset+sizesso mobile doesn't download a 2000px image. - Width/height attributes to reserve space (helps CLS too).
- Don't lazy-load the LCP image —
loading="lazy"on a hero is a common bug; mark itloading="eager"or omit.
Reduce render-blocking
- Inline critical CSS for above-the-fold; load the rest async (
<link rel="preload" as="style">+ onload swap, or a critical-CSS tool). async/defernon-critical JS.mediaattribute on stylesheets to mark non-blocking.
Faster TTFB
- CDN with edge caching.
- SSR/SSG to ship HTML with the hero in it — preload scanner sees it immediately.
- HTTP/2 or 3 for multiplexing.
- Server response budget — aim for < 500ms TTFB.
Reduce JS work
- Code-split — don't ship all routes' JS upfront.
- Tree-shake dependencies.
- Defer hydration for non-interactive parts (Astro / React Server Components).
- Skip polyfills for modern browsers (
module/nomodule).
Fonts
font-display: swapso text renders in fallback while web font loads.- Preload critical font.
- Subset to only needed glyphs.
Measure it
- PageSpeed Insights / Lighthouse — synthetic LCP.
- Web Vitals library in production for RUM:
import { onLCP } from "web-vitals";
onLCP((metric) => send("/rum", metric));- Watch p75 in real user data — synthetic doesn't catch device/network variance.
- Element-level: Chrome DevTools Performance panel highlights the LCP element.
Interview framing
"LCP is when the largest visible content element finishes painting — usually a hero image or large headline — and the target is < 2.5s at p75. It's slowed by server response time, render-blocking CSS/JS, large or late-discovered images, and client-side rendering. The wins, in order of impact: fix TTFB with a CDN and SSR; preload the hero image with fetchpriority='high'; use modern formats and responsive sizes; inline critical CSS and defer non-critical JS; never lazy-load the LCP. Measure in production with the web-vitals library at p75 — synthetic numbers lie."
Follow-up questions
- •Why is preload + fetchpriority='high' better than lazy-load for the hero?
- •Why is CSS background-image bad for the LCP element?
- •How does SSR change LCP compared to CSR?
- •What does font-display: swap do?
Common mistakes
- •Lazy-loading the hero image.
- •Background-image for the LCP element.
- •Huge unoptimized JPEG/PNG hero.
- •CSR with a slow JS bundle — LCP waits for hydration.
- •Trusting Lighthouse over RUM at p75.
Performance considerations
- •LCP is the whole topic. Pair with INP and CLS — they tradeoff (e.g., heavy preloads can hurt INP).
Edge cases
- •LCP element changes after initial render.
- •Hero behind authentication — different LCP for logged-in vs out.
- •Slow 3G mobile — biggest gap between synthetic and real.
Real-world examples
- •Next.js `<Image priority>` for the hero — automatically does preload + fetchpriority.
- •Chrome DevTools Performance panel LCP marker.