Web Performance
LCP: how to optimize Largest Contentful Paint

LCP is the core loading performance metric in Core Web Vitals. It measures how quickly users see the main content of a page. We cover what drives LCP, why it slows down, and how to get it into the green threshold on real-world projects.
Largest Contentful Paint (LCP) is the Core Web Vitals metric that has influenced Google rankings since 2021. It is not just a technical number — it reflects how quickly users perceive a page as loaded. A poor LCP means visitors see a blank screen or an unloaded hero image and leave before the content appears.
What is LCP and why it matters
LCP measures the time from the start of navigation to the moment the largest content element (text or image) becomes visible in the viewport. This is the threshold Google uses to consider loading 'done' from a user experience standpoint.
Unlike FCP (First Contentful Paint), which fires on the first pixel, LCP tracks meaningful content specifically. A page might show a spinner at 0.3 s, but if the hero image appears at 5 s — the LCP is 5 s.
Good LCP
Google's green threshold. The target for most pages
Needs improvement
Yellow zone — LCP is noticeable to users, optimisation needed
Poor LCP
Red zone — significant damage to UX and rankings
CrUX percentile
Google evaluates LCP at the 75th percentile of real Chrome users
Which elements the browser considers LCP candidates
The browser does not track every element — it watches specific types and selects the largest by area within the viewport at the time loading completes.
- <img> elements
- Standard images. The most common LCP candidate on most pages. Time = moment the decoded image appears in the viewport.
- <image> inside SVG
- Images within SVG elements. Treated the same as regular img elements for LCP purposes.
- CSS background-image
- Background images via CSS. Key nuance: the browser does not start loading a background-image until the CSSOM is built. This is one of the primary sources of LCP delay.
- Text blocks
- <p>, <h1>–<h6>, <div>, and other elements with text content. If a text block is larger than any image, it becomes the LCP.
- <video> with poster
- The poster frame of a video element (poster attribute). If a video occupies most of the screen, the poster must load as fast as possible.
How LCP time breaks down
Google decomposes LCP into 4 sub-components. Understanding each one is key to correctly diagnosing and prioritising optimisations.
| Component | What it measures | Typical share |
|---|---|---|
| TTFB (Time to First Byte) | Time until the first byte of HTML arrives from the server | 10–40% |
| Resource Load Delay | Gap between TTFB and when the LCP resource starts loading | 10–30% |
| Resource Load Duration | Time to download the LCP resource itself (image, font) | 30–60% |
| Element Render Delay | Gap between resource load completion and its appearance in viewport | 0–20% |
Rule: attack the component with the largest share. If TTFB accounts for 3 of 5 seconds of LCP — optimising images will have minimal impact. Fix server response time first.
Main causes of slow LCP
90% of poor LCP scores share the same root causes. Diagnosis takes 15 minutes in DevTools or PageSpeed Insights.
Slow TTFB
Server responds in more than 600 ms. Causes: no caching, weak hosting, heavy database queries, no CDN. With TTFB > 800 ms, achieving LCP < 2.5 s is nearly impossible without other optimisations.
Unoptimised images
The LCP image weighs 500 KB+ as JPEG instead of 80 KB as WebP/AVIF. No fetchpriority="high" attribute, no preload. The browser discovers the image late and loads it slowly.
Render-blocking resources
CSS and JS in <head> without async/defer block rendering. Until the browser loads and executes them, the LCP element cannot appear. Every extra script in head adds delay.
Slow font loading
If LCP is a text block and the font takes 2+ seconds to load, the browser shows FOIT (invisible text) or FOUT (system font). The time until the correct text appears equals the LCP.
Lazy load on the LCP element
The loading="lazy" attribute on the main hero image is one of the most common mistakes. The browser deliberately defers loading — a direct hit to LCP.
LCP in client-side rendering
In SPAs (React, Vue, Angular) without SSR, content is generated in the browser via JavaScript. The browser sees empty HTML → loads JS → renders content. The entire chain accumulates into LCP.
How to optimise LCP: a step-by-step plan
LCP optimisation is not a single trick — it is a set of interrelated improvements. Work sequentially, starting with the highest-impact items.
Step 1: Reduce TTFB to 200–400 ms
TTFB is the foundation. If the server is slow, all other optimisations work against that ceiling.
- Configure HTTP caching: Cache-Control: max-age=31536000 for static assets, s-maxage for CDN proxies.
- Enable compression: Brotli (preferred) or gzip for HTML, CSS, JS. Brotli delivers 15–25% smaller files than gzip.
- Connect a CDN: Cloudflare, Fastly, BunnyCDN. For static sites, edge caching reduces TTFB to 20–50 ms.
- Optimise the database: indexes on frequently queried fields, query caching via Redis or Memcached.
- Use HTTP/2 or HTTP/3: multiple requests over a single connection with no head-of-line blocking.
Step 2: Optimise the LCP image
If the LCP element is an image (hero, product image), this is a high-ROI optimisation tree.
- Convert to modern formats: WebP (25–35% savings vs JPEG) or AVIF (40–55% vs JPEG). Use <picture> with a fallback.
- Add fetchpriority="high" to the LCP image — the browser will load it with top priority.
- Remove loading="lazy" from the LCP element. Lazy load is only appropriate for images below the first viewport.
- Add <link rel="preload" as="image"> in <head> for the LCP image — the browser starts loading before body is parsed.
- Specify width and height attributes to prevent CLS. The browser reserves space before the image loads.
- Configure srcset and sizes for responsive loading: mobile gets a 400 px image instead of a 1600 px one.
Step 3: Eliminate render-blocking resources
Every render-blocking resource adds delay to LCP. The goal: zero blocking resources in the critical path.
| Resource type | Problem | Solution |
|---|---|---|
| JavaScript in <head> | Browser stops HTML parsing and executes the script | defer or async on all non-critical scripts |
| CSS in <head> | Browser renders nothing until all CSS is loaded | Critical CSS inline + async loadCSS for non-critical styles |
| Third-party scripts | Analytics, chat bots, pixels loading in head | Move to body or load with delay (setTimeout 3000) |
| Fonts without preload | Browser discovers the font only after parsing CSS | <link rel="preload" as="font"> + font-display: swap |
Step 4: Optimise fonts (when LCP is text)
When the LCP element is a text block (heading, lead), the problem is often font render delay.
- Add font-display: swap or optional to @font-face. The browser shows the system font immediately and swaps in the custom one after loading.
- Use <link rel="preload" as="font" type="font/woff2" crossorigin> for critical fonts in <head>.
- Limit the character set: use subsetting to load only the needed glyphs (Latin, Cyrillic).
- Self-host fonts instead of loading from Google Fonts — eliminates an extra DNS lookup and connection.
- Configure size-adjust, ascent-override, descent-override in @font-face to reduce CLS during font swap.
Step 5: SSR/SSG for JavaScript frameworks
If the site runs on React, Vue, or Next.js without SSR — LCP will be high by default: the browser must download, execute JavaScript, and only then render the content.
- Server-Side Rendering (SSR)
- HTML is generated on the server for each request. The browser receives ready HTML, parses it, and the LCP element appears without waiting for JS. Best option for dynamic content.
- Static Site Generation (SSG)
- HTML is generated at build time. The server delivers a ready file — maximum TTFB and LCP speed. Ideal for blogs, landing pages, and non-personalised pages.
- Incremental Static Regeneration (ISR)
- SSG with periodic regeneration (Next.js). Combines the speed of static files with data freshness. Optimal for most CMS content.
- Streaming SSR
- The server starts sending HTML before rendering completes — the browser progressively displays content. React 18 + Next.js 13+ support this out of the box.
Step 6: CSS background-image as LCP
If the LCP element uses a CSS background-image, the browser cannot start loading it until the CSSOM is built. This is one of the most insidious causes of slow LCP.
- Replace CSS background-image with <img fetchpriority="high"> — the browser discovers it during HTML parsing.
- If replacement is not possible — use <link rel="preload" as="image"> to start loading manually.
- For responsive backgrounds use imagesrcset and imagesizes in the preload tag.
- Move the CSS containing the LCP background-image into an inline <style> — eliminates one network request.
Tools for measuring and diagnosing LCP
Correct diagnosis matters more than blind optimisation. Use both tool types: Lab (synthetic tests) and Field (real users).
| Tool | Data type | What it provides | When to use |
|---|---|---|---|
| PageSpeed Insights | Lab + Field (CrUX) | LCP score, sub-component breakdown, specific recommendations | Initial audit, verifying changes |
| Chrome DevTools Performance | Lab | Load waterfall, exact cause of LCP delay, flame chart | Deep diagnosis of a specific problem |
| Lighthouse CI | Lab | Automated checks in CI/CD, LCP history by release | Regression control |
| WebPageTest | Lab | Tests from multiple regions, video comparison, filmstrip, waterfall | CDN verification, regional LCP |
| Google Search Console (CWV) | Field (CrUX) | Real LCP by URL groups, pages with issues | Production monitoring |
| web-vitals.js | Field | JavaScript API for collecting LCP from real users into custom analytics | Custom monitoring, RUM |
How to diagnose LCP in Chrome DevTools
DevTools is the most precise tool for understanding exactly what is slowing LCP on a specific page.
- Open DevTools → Performance → press Record, reload the page, stop the recording.
- In Timings find the LCP marker — it shows the exact time and the element.
- Study the waterfall: find the LCP element's resource and check when its loading started relative to TTFB.
- Look for red bars — these are render-blocking resources. They appear right before LCP.
- Network tab → Filter by type Image — find the LCP image and check its priority (should be High).
- Coverage tab — shows unused CSS/JS. Large amounts of unused code = LCP delay.