LCP Image Optimization

Image Optimization to Fix LCP: A Complete Guide

Images are the LCP element on 70-80% of web pages. When your hero image, product photo, or featured graphic is the largest visible element, its load time directly determines your LCP score. An unoptimized image can single-handedly push LCP from "good" to "poor."

The HTTP Archive reports that the median web page loads 900KB of images, with the top 10% loading over 4MB. Modern image optimization techniques can reduce this by 60-80% without visible quality loss, directly translating to faster LCP.

This guide covers five optimization layers: modern formats, responsive sizing, priority loading, CDN delivery, and automated optimization pipelines. Applied together, they typically reduce LCP image load time from 3-5 seconds to under 1.5 seconds.

Expected results

Following all steps in this guide typically produces these improvements:

Before

5.1s

LCP score (Poor) -- unoptimized hero image: 2.4MB JPEG, no responsive sizing, no CDN

After

1.6s

LCP score (Good) -- AVIF format, responsive sizes, priority preload, edge CDN delivery

Step-by-step fix

Convert to modern formats: AVIF and WebP

AVIF offers 50% smaller files than JPEG at equivalent visual quality, while WebP offers 25-30% savings. Using the <picture> element with format fallbacks ensures every browser gets the smallest file it supports. AVIF is supported in Chrome, Firefox, and Safari 16+; WebP in all modern browsers.

HTML -- Picture element with format fallback
<!-- Progressive format delivery: best format first -->
<picture>
  <!-- AVIF: smallest file, newest format -->
  <source
    srcset="/images/hero-400.avif 400w,
            /images/hero-800.avif 800w,
            /images/hero-1200.avif 1200w,
            /images/hero-1600.avif 1600w"
    sizes="100vw"
    type="image/avif"
  />
  <!-- WebP: good compression, wide support -->
  <source
    srcset="/images/hero-400.webp 400w,
            /images/hero-800.webp 800w,
            /images/hero-1200.webp 1200w,
            /images/hero-1600.webp 1600w"
    sizes="100vw"
    type="image/webp"
  />
  <!-- JPEG: universal fallback -->
  <img
    src="/images/hero-1200.jpg"
    srcset="/images/hero-400.jpg 400w,
            /images/hero-800.jpg 800w,
            /images/hero-1200.jpg 1200w,
            /images/hero-1600.jpg 1600w"
    sizes="100vw"
    alt="Product showcase hero image"
    width="1600"
    height="900"
    fetchpriority="high"
    decoding="async"
  />
</picture>
Bash -- Generate all formats with Sharp
# install sharp-cli globally
npm install -g sharp-cli

# Convert a single image to all formats and sizes
for SIZE in 400 800 1200 1600; do
  # AVIF (quality 60 looks equivalent to JPEG quality 80)
  sharp -i hero.jpg -o hero-${SIZE}.avif     --format avif --quality 60 --resize ${SIZE}

  # WebP
  sharp -i hero.jpg -o hero-${SIZE}.webp     --format webp --quality 75 --resize ${SIZE}

  # Optimized JPEG fallback
  sharp -i hero.jpg -o hero-${SIZE}.jpg     --format jpeg --quality 80 --resize ${SIZE}     --mozjpeg
done

Implement responsive images with correct sizes attribute

The srcset attribute tells the browser which image sizes are available, and the sizes attribute tells it how large the image will be at different viewport widths. Without sizes, the browser defaults to assuming the image is 100vw wide and downloads unnecessarily large files on small screens.

Always include width and height: Without explicit dimensions, the browser cannot calculate the aspect ratio before the image loads, causing Cumulative Layout Shift. Set width and height attributes matching the image's intrinsic ratio.
HTML -- Responsive images with accurate sizes
<!-- Hero image: full width on mobile, constrained on desktop -->
<img
  src="/images/hero-1200.jpg"
  srcset="/images/hero-400.jpg 400w,
          /images/hero-800.jpg 800w,
          /images/hero-1200.jpg 1200w,
          /images/hero-1600.jpg 1600w"
  sizes="(min-width: 1280px) 1200px,
         (min-width: 768px) 90vw,
         100vw"
  alt="Descriptive alt text"
  width="1600"
  height="900"
  fetchpriority="high"
/>

<!-- Sidebar image: always small -->
<img
  src="/images/sidebar-400.jpg"
  srcset="/images/sidebar-200.jpg 200w,
          /images/sidebar-400.jpg 400w"
  sizes="(min-width: 768px) 300px, 100vw"
  alt="Sidebar illustration"
  width="400"
  height="300"
  loading="lazy"
/>

Use fetchpriority=high and preload for the LCP image

By default, the browser discovers images only after parsing HTML and CSS. For the LCP image, you need to tell the browser to start downloading it immediately. The fetchpriority="high" attribute boosts the image's priority in the browser's request queue, and a <link rel="preload"> in the <head> starts the download even earlier.

HTML -- Preload and priority hints
<head>
  <!-- Preload the LCP image with responsive srcset -->
  <link
    rel="preload"
    as="image"
    href="/images/hero-1200.avif"
    imagesrcset="/images/hero-400.avif 400w,
                 /images/hero-800.avif 800w,
                 /images/hero-1200.avif 1200w,
                 /images/hero-1600.avif 1600w"
    imagesizes="(min-width: 1280px) 1200px,
                (min-width: 768px) 90vw,
                100vw"
    type="image/avif"
    fetchpriority="high"
  />
</head>

<body>
  <!-- The img element matches the preload -->
  <img
    src="/images/hero-1200.avif"
    srcset="/images/hero-400.avif 400w,
            /images/hero-800.avif 800w,
            /images/hero-1200.avif 1200w,
            /images/hero-1600.avif 1600w"
    sizes="(min-width: 1280px) 1200px,
           (min-width: 768px) 90vw,
           100vw"
    alt="Hero image"
    width="1600"
    height="900"
    fetchpriority="high"
    decoding="async"
  />
</body>

Serve images from an edge CDN with proper caching

An edge CDN delivers images from the server closest to the user, reducing latency from 200-500ms (origin server) to 10-50ms (edge node). Combined with aggressive caching and on-the-fly image transformation, a CDN can handle format conversion, resizing, and compression automatically.

JavaScript -- Cloudflare Image Resizing (or similar CDN)
// Image URL pattern with CDN transformation
function getImageUrl(src, { width, format, quality }) {
  // Cloudflare Image Resizing
  return `/cdn-cgi/image/width=${width},format=${format},quality=${quality}/${src}`;

  // Or Imgix
  // return `https://your-domain.imgix.net/${src}?w=${width}&fm=${format}&q=${quality}`;

  // Or Cloudinary
  // return `https://res.cloudinary.com/your-cloud/image/upload/w_${width},f_${format},q_${quality}/${src}`;
}

// Usage in HTML generation
const heroUrl = getImageUrl('hero.jpg', {
  width: 1200,
  format: 'auto',  // CDN serves AVIF/WebP based on Accept header
  quality: 75,
});
HTTP Headers -- Cache configuration
# Immutable images (hashed filenames like hero-abc123.avif)
Cache-Control: public, max-age=31536000, immutable

# Mutable images (same filename, content may change)
Cache-Control: public, max-age=86400, stale-while-revalidate=604800

# CDN-specific: cache at edge for 1 year, browser for 1 day
Cache-Control: public, max-age=86400, s-maxage=31536000

# Vary header for format negotiation
Vary: Accept

Build an automated image optimization pipeline

Manual image optimization does not scale. An automated pipeline processes images at build time or upload time, generating all required formats, sizes, and quality levels. This ensures every image on your site is optimized without relying on individual developers to remember optimization steps.

JavaScript -- Build-time optimization with Sharp
// scripts/optimize-images.mjs
import sharp from 'sharp';
import { glob } from 'glob';
import path from 'path';
import fs from 'fs/promises';

const WIDTHS = [400, 800, 1200, 1600];
const FORMATS = [
  { ext: 'avif', options: { quality: 60, effort: 4 } },
  { ext: 'webp', options: { quality: 75 } },
  { ext: 'jpg',  options: { quality: 80, mozjpeg: true } },
];

async function optimizeImage(inputPath, outputDir) {
  const name = path.basename(inputPath, path.extname(inputPath));
  const image = sharp(inputPath);
  const metadata = await image.metadata();

  const tasks = [];

  for (const width of WIDTHS) {
    // Skip sizes larger than original
    if (width > metadata.width) continue;

    for (const format of FORMATS) {
      const outputPath = path.join(
        outputDir,
        `${name}-${width}.${format.ext}`
      );

      tasks.push(
        sharp(inputPath)
          .resize(width)
          .toFormat(format.ext, format.options)
          .toFile(outputPath)
          .then(info => {
            const savings = (
              (1 - info.size / metadata.size) * 100
            ).toFixed(0);
            console.log(
              `  ${name}-${width}.${format.ext}: ` +
              `${(info.size / 1024).toFixed(0)}KB (${savings}% smaller)`
            );
          })
      );
    }
  }

  await Promise.all(tasks);
}

// Process all source images
const images = await glob('src/images/**/*.{jpg,jpeg,png}');
console.log(`Optimizing ${images.length} images...`);

for (const img of images) {
  console.log(`\nProcessing: ${img}`);
  await optimizeImage(img, 'public/images/');
}

console.log('\nDone! All images optimized.');

Quick checklist

  • LCP image served in AVIF with WebP and JPEG fallbacks
  • Responsive srcset with accurate sizes attribute
  • LCP image has fetchpriority="high" and <link rel="preload">
  • All images include explicit width and height attributes
  • Below-fold images use loading="lazy"
  • Images served from edge CDN with proper caching headers
  • Automated optimization pipeline in build process

Frequently asked questions

AVIF typically achieves 50% smaller files than JPEG at equivalent visual quality, and 20-30% smaller than WebP. For a typical hero image, a 300KB JPEG becomes approximately 150KB in AVIF and 210KB in WebP. The savings are even greater for images with large areas of similar color (illustrations, gradients, screenshots).

Never lazy load the LCP image. Lazy loading (loading=lazy) defers the image download until it enters the viewport, which directly delays LCP by hundreds of milliseconds. The LCP image should use fetchpriority=high and optionally a preload link to start downloading as early as possible. Only use lazy loading for below-the-fold images.

Yes, significantly. A CDN reduces image delivery latency from 200-500ms (origin server in one region) to 10-50ms (edge node near the user). For the LCP image, this directly translates to 150-450ms LCP improvement. CDNs also handle format negotiation (serving AVIF to supported browsers) and on-the-fly resizing.

Generate sizes at common breakpoints: 400w, 800w, 1200w, and 1600w cover most use cases. This handles mobile (400px viewport), tablet (800px), desktop (1200px), and high-DPI desktop (1600px+). For hero images displayed at full viewport width, you may want to add 2000w and 2400w for 4K displays. Each additional size increases build time and storage but improves download efficiency.

Next.js provides the Image component (next/image) that handles responsive sizing, format conversion (WebP/AVIF), and lazy loading automatically. For the LCP image, add the priority prop to disable lazy loading and enable preloading. Use the sizes prop to match your layout. The built-in image optimization runs at request time and caches results, so no build-time pipeline is needed.

Related resources