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:

ScoreRating
≤ 2.5sGood (green)
2.5s - 4.0sNeeds Improvement (orange)
> 4.0sPoor (red)

How to Measure LCP?

Lab Tools (Lab data)

  1. Lighthouse (DevTools → Lighthouse)
  2. PageSpeed Insights - pagespeed.web.dev
  3. WebPageTest - webpagetest.org
  4. Chrome DevTools → Performance panel

Field Data (Real User Data)

  1. Google Search Console → Core Web Vitals
  2. Chrome UX Report (CrUX) - real data from Chrome users
  3. 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:

  1. Open the Performance tab
  2. Press Ctrl+Shift+E (record with reload)
  3. Look for the LCP marker on the timeline
  4. 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

FormatUse CaseCompression
WebPUniversal, great compression25-35% smaller than JPEG
AVIFBest compression, but slower decoding50% smaller than JPEG
JPEGFallback for older browsersBaseline
PNGGraphics with transparencyLossless
<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 */
}
ValueBehavior
swapImmediate fallback, swap after loading
optionalShort fallback, may not load font
fallbackShort fallback (100ms), then swap
blockInvisible 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:

  1. Identify the LCP element - usually hero image
  2. Preload critical resources - LCP image, fonts
  3. Optimize images - format, size, responsiveness
  4. Eliminate blocking resources - Critical CSS, defer JS
  5. 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

  1. web.dev - Largest Contentful Paint (LCP) https://web.dev/articles/lcp

  2. web.dev - Optimize LCP https://web.dev/articles/optimize-lcp

  3. Google Search Central - Core Web Vitals https://developers.google.com/search/docs/appearance/core-web-vitals

  4. Chrome Developers - Fetch Priority API https://developer.chrome.com/docs/lighthouse/performance/priority-hints

  5. web.dev - Preload critical assets https://web.dev/articles/preload-critical-assets

  6. MDN - Resource Hints https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload