Largest Contentful Paint (LCP) is one of the three key Core Web Vitals metrics that measures the loading time of the largest visible element on a page. Google uses LCP as a ranking factor, and a poor score can negatively impact your search result positions.
What is LCP?
LCP measures the time from the start of page loading until the largest element in the visible area (viewport) is rendered. Typically, this element is:
- Hero image
- Large text block
<video>element with a poster- Element with a background image (background-image)
LCP Thresholds
Google defines the following thresholds:
| Score | Rating |
|---|---|
| ≤ 2.5s | Good (green) |
| 2.5s - 4.0s | Needs Improvement (orange) |
| > 4.0s | Poor (red) |
How to Measure LCP?
Lab Tools (Lab data)
- Lighthouse (DevTools → Lighthouse)
- PageSpeed Insights - pagespeed.web.dev
- WebPageTest - webpagetest.org
- Chrome DevTools → Performance panel
Field Data (Real User Data)
- Google Search Console → Core Web Vitals
- Chrome UX Report (CrUX) - real data from Chrome users
- web-vitals library - measurements on your own site
import { onLCP } from 'web-vitals';
onLCP(console.log);
// { name: 'LCP', value: 2547, rating: 'needs-improvement' }
Identifying the LCP Element
In Chrome DevTools:
- Open the Performance tab
- Press Ctrl+Shift+E (record with reload)
- Look for the LCP marker on the timeline
- Click to see which element is LCP
Main Causes of Slow LCP
1. Slow Server Response Time (TTFB)
Time to First Byte is the time from sending the request to receiving the first byte of response. If TTFB is high, LCP will automatically be delayed.
Solutions:
- Use a CDN (Cloudflare, Fastly, AWS CloudFront)
- Enable Brotli/Gzip compression
- Optimize database queries
- Use server-side caching (Redis, Memcached)
- Consider SSG instead of SSR for static pages
2. Render-blocking Resources
CSS and JavaScript in <head> block page rendering.
Solutions:
<!-- Instead of -->
<link rel="stylesheet" href="styles.css">
<!-- Use inline critical CSS -->
<style>
/* Critical CSS for above-the-fold */
.hero { ... }
</style>
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<!-- JavaScript - use defer or async -->
<script defer src="app.js"></script>
<script async src="analytics.js"></script>
3. Slow Resource Loading
Images, fonts, and other resources must be downloaded before LCP is rendered.
4. Client-side Rendering
SPAs (Single Page Applications) render content after JavaScript loads, which delays LCP.
Solutions:
- Use SSR (Server-Side Rendering)
- Pre-rendering / SSG (Static Site Generation)
- Progressive Hydration
Image Optimization - Key to Good LCP
Images are the most common LCP element. Their optimization has the biggest impact on scores.
Image Formats
| Format | Use Case | Compression |
|---|---|---|
| WebP | Universal, great compression | 25-35% smaller than JPEG |
| AVIF | Best compression, but slower decoding | 50% smaller than JPEG |
| JPEG | Fallback for older browsers | Baseline |
| PNG | Graphics with transparency | Lossless |
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image" width="1200" height="600">
</picture>
Responsive Images
Don’t load a 4K image on a phone!
<img
src="hero-800.jpg"
srcset="
hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w,
hero-1600.jpg 1600w
"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
alt="Hero image"
width="1200"
height="600"
>
Lazy Loading vs Eager Loading
The LCP element should NOT have loading="lazy"!
<!-- Hero image (LCP) - load immediately -->
<img src="hero.jpg" alt="Hero" fetchpriority="high">
<!-- Images below the fold - lazy load -->
<img src="product.jpg" alt="Product" loading="lazy">
Image Dimensions
Always specify width and height to avoid layout shift:
<img src="hero.jpg" width="1200" height="600" alt="Hero">
Preload - Speeding Up Critical Resources
Preload tells the browser that a resource will be needed soon and should be downloaded with high priority.
Preload for LCP Image
<head>
<link rel="preload" as="image" href="hero.webp" type="image/webp">
<!-- Or responsively -->
<link
rel="preload"
as="image"
href="hero-mobile.webp"
media="(max-width: 600px)"
>
<link
rel="preload"
as="image"
href="hero-desktop.webp"
media="(min-width: 601px)"
>
</head>
Preload for Fonts
Fonts often delay text rendering:
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
>
Preconnect to External Domains
If the LCP image is on a CDN, establish the connection earlier:
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
Fetch Priority API
Modern browsers support fetchpriority for controlling resource priority:
<!-- High priority for LCP image -->
<img src="hero.jpg" fetchpriority="high" alt="Hero">
<!-- Low priority for less important images -->
<img src="decoration.jpg" fetchpriority="low" alt="Decoration">
<!-- High priority for critical CSS -->
<link rel="stylesheet" href="critical.css" fetchpriority="high">
CSS Optimization
Critical CSS Inline
Extract CSS needed for rendering above-the-fold and insert inline:
<head>
<style>
/* Critical CSS - viewport only */
header { ... }
.hero { ... }
nav { ... }
</style>
<!-- Rest of CSS asynchronously -->
<link rel="preload" href="main.css" as="style" onload="this.rel='stylesheet'">
</head>
Tools for extracting Critical CSS:
- Critical (npm package)
- Critters (webpack plugin)
- PurgeCSS (removing unused CSS)
Avoid @import
/* Bad - creates request chain */
@import url('fonts.css');
@import url('components.css');
/* Good - use <link> in HTML */
Minification and Compression
# CSS minification
npx csso styles.css -o styles.min.css
# Brotli compression
brotli -f styles.min.css
JavaScript Optimization
Removing Render-blocking JS
<!-- Bad - blocks rendering -->
<script src="app.js"></script>
<!-- Good - defer for main JS -->
<script defer src="app.js"></script>
<!-- Async for independent scripts -->
<script async src="analytics.js"></script>
Code Splitting
Don’t load the entire bundle on every page:
// React - lazy loading components
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
// Dynamic import
if (userClickedButton) {
const module = await import('./feature.js');
}
Tree Shaking
Make sure your bundler removes unused code:
// Import only what you need
import { debounce } from 'lodash-es'; // Good
import _ from 'lodash'; // Bad - imports entire library
Font Optimization
Font-display
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* Show fallback font, swap when loaded */
}
| Value | Behavior |
|---|---|
swap | Immediate fallback, swap after loading |
optional | Short fallback, may not load font |
fallback | Short fallback (100ms), then swap |
block | Invisible text until loaded (FOIT) |
Font Subsetting
Load only the characters you need:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153; /* Latin only */
}
Tools:
- Glyphhanger - automatic subsetting
- Google Fonts -
&text=or&subset=parameter
Self-hosting Fonts
Hosting fonts on your own server eliminates external requests:
<!-- Instead of Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet">
<!-- Host locally -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
Server and CDN Optimization
Enable HTTP/2 or HTTP/3
HTTP/2 allows parallel resource loading:
# nginx.conf
server {
listen 443 ssl http2;
# ...
}
Cache Headers
# Long cache for static resources
location ~* \.(js|css|png|jpg|webp|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Brotli Compression
# nginx.conf
brotli on;
brotli_comp_level 6;
brotli_types text/html text/css application/javascript image/svg+xml;
Edge Caching (CDN)
Use a CDN with edge locations close to users:
- Cloudflare
- Fastly
- AWS CloudFront
- Vercel Edge Network
LCP Optimization Checklist
Priority 1 - Most Important
- Identify the LCP element on the page
- Preload for LCP image/resource
-
fetchpriority="high"for LCP element - Remove
loading="lazy"from LCP element - Optimize images (WebP/AVIF, compression)
Priority 2 - Important
- Critical CSS inline
- Defer/async for JavaScript
- Preconnect to external domains
- Responsive images (srcset)
- font-display: swap for fonts
Priority 3 - Additional
- HTTP/2 or HTTP/3
- Brotli compression
- CDN for static resources
- Self-hosting fonts
- JavaScript code splitting
Summary
LCP is a critical performance metric that directly impacts user experience and Google rankings. Key actions:
- Identify the LCP element - usually hero image
- Preload critical resources - LCP image, fonts
- Optimize images - format, size, responsiveness
- Eliminate blocking resources - Critical CSS, defer JS
- Improve TTFB - CDN, cache, server optimization
Regular monitoring of LCP in Google Search Console and Lighthouse will help maintain good results and respond to issues. The goal is under 2.5 seconds for 75% of users.
Sources
-
web.dev - Largest Contentful Paint (LCP) https://web.dev/articles/lcp
-
web.dev - Optimize LCP https://web.dev/articles/optimize-lcp
-
Google Search Central - Core Web Vitals https://developers.google.com/search/docs/appearance/core-web-vitals
-
Chrome Developers - Fetch Priority API https://developer.chrome.com/docs/lighthouse/performance/priority-hints
-
web.dev - Preload critical assets https://web.dev/articles/preload-critical-assets
-
MDN - Resource Hints https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload



