Astro Image Optimization: Fix LCP Without the Guesswork

Your Astro site ships zero JavaScript. Your Lighthouse performance score is green. And your LCP is still 4.2 seconds.

The problem isn't Astro. It's your images.

Astro's zero-JS architecture eliminates the most common cause of slow sites — render-blocking JavaScript. But LCP measures when the largest visible element finishes rendering, and that element is almost always an image. A 1.8MB uncompressed JPEG doesn't care how little JavaScript you ship. It's still going to take 3 seconds to download on a 4G connection.

This guide walks through the five image mistakes that most commonly hurt LCP on Astro sites, including a default behavior that works against you out of the box. Every fix includes before-and-after measurements so you can prioritize based on impact.

Here's what a typical unoptimized Astro site looks like:

MetricValue
LCP4.2s
CLS0.14
INP45ms
Hero image size1.8MB
PageSpeed mobile52

Notice the INP is already excellent — that's Astro's zero-JS advantage. But the LCP and CLS scores are dragging the overall score down, and both are caused entirely by images.

Fix 1: The lazy loading trap (saved 1.4s LCP)

This is the most impactful fix, and it catches almost everyone.

Astro's <Image /> component defaults to loading="lazy" and decoding="async". For below-the-fold images, this is exactly right. For your hero image — the one that's almost always your LCP element — it's a performance killer.

The problem

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<!-- This looks correct but hurts LCP -->
<Image src={heroImage} alt="Product hero shot" />

With loading="lazy", the browser won't start fetching the image until it enters the viewport. Since the hero image is above the fold, the browser has to:

  1. Download the HTML
  2. Parse the DOM
  3. Run layout to determine the image is in the viewport
  4. Then start the image request

That layout step adds 200-400ms before the image download even begins. On a mid-tier mobile device, it's worse.

The fix

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<!-- Explicitly override defaults for LCP images -->
<Image
  src={heroImage}
  alt="Product hero shot"
  loading="eager"
  fetchpriority="high"
/>

Two attributes change everything:

  • loading="eager" tells the browser to start fetching immediately, not wait for layout
  • fetchpriority="high" tells the browser this image is more important than other resources, so it should be prioritized in the network queue

Unlike Next.js, Astro doesn't have a priority prop that handles both of these for you. You need to set them manually.

Add a preload hint for maximum speed

For the absolute fastest LCP, add a preload link in your layout's <head>:

---
// src/layouts/Layout.astro
---

<html>
  <head>
    <link
      rel="preload"
      href="/optimized-hero.webp"
      as="image"
      type="image/webp"
      fetchpriority="high"
    />
  </head>
  <body>
    <slot />
  </body>
</html>

Preloading starts the image download before the browser even encounters the <img> tag in the DOM. Combined with loading="eager", this eliminates all unnecessary delay.

Results

MetricBeforeAfterChange
LCP4.2s2.8s-1.4s
Image load start1.2s0.3s-900ms

1.4 seconds from two HTML attributes. This single fix is often the difference between a "poor" and "needs improvement" LCP score.

Fix 2: Use Picture for format negotiation (saved 340KB)

Astro's <Image /> component outputs WebP by default. That's good — WebP is 25-30% smaller than JPEG. But AVIF is 20-30% smaller than WebP, and 93% of browsers now support it.

The problem

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<!-- Only serves WebP — misses AVIF savings -->
<Image src={heroImage} alt="Product hero shot" loading="eager" />

This outputs a single WebP file. Browsers that support AVIF (Chrome, Firefox, Safari 16+) still get the larger WebP.

The fix

---
import { Picture } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<Picture
  src={heroImage}
  formats={['avif', 'webp']}
  alt="Product hero shot"
  loading="eager"
  fetchpriority="high"
/>

This generates a <picture> element with multiple <source> tags. The browser picks AVIF if it can, falls back to WebP, and uses the original format as a last resort.

Real file sizes

For a 1600x900 hero image at default quality:

FormatFile sizevs Original JPEG
JPEG (original)485KBbaseline
WebP192KB-60%
AVIF145KB-70%

The AVIF version is 340KB smaller than the original and 47KB smaller than WebP. On a 4G connection, that's roughly 200ms faster to download.

Results

MetricBeforeAfterChange
Hero image size485KB (JPEG)145KB (AVIF)-70%
LCP2.8s2.4s-400ms

Fix 3: Responsive images for mobile (saved 600ms LCP on mobile)

A 1600px-wide hero image is perfect for desktop. On a 375px-wide phone screen, you're downloading 4x more pixels than the screen can display. That's wasted bytes and wasted time.

The problem

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<!-- Same large image sent to all devices -->
<Image
  src={heroImage}
  alt="Product hero shot"
  loading="eager"
  fetchpriority="high"
/>

Without responsive sizing, every device downloads the full-resolution image.

The fix (Astro 5.10+)

Astro's responsive image layout generates srcset and sizes automatically:

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<Image
  src={heroImage}
  alt="Product hero shot"
  loading="eager"
  fetchpriority="high"
  layout="full-width"
/>

The layout="full-width" prop tells Astro to generate multiple sizes and let the browser pick the right one based on the viewport. No manual srcset needed.

Enable the global responsive styles in your config:

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  image: {
    responsiveStyles: true,
  },
});

For more control over breakpoints, use the widths prop with <Picture>:

---
import { Picture } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<Picture
  src={heroImage}
  formats={['avif', 'webp']}
  widths={[400, 800, 1200, 1600]}
  sizes="(max-width: 600px) 400px, (max-width: 1024px) 800px, (max-width: 1400px) 1200px, 1600px"
  alt="Product hero shot"
  loading="eager"
  fetchpriority="high"
/>

Mobile impact

DeviceImage downloadedBeforeAfter
Desktop (1440px)1600w AVIF145KB145KB
Tablet (768px)800w AVIF145KB62KB
Phone (375px)400w AVIF145KB28KB

On mobile, the image is 80% smaller. That translates directly to LCP improvement.

Results

MetricBefore (mobile)After (mobile)Change
Hero image (mobile)145KB28KB-80%
LCP (mobile)2.4s1.8s-600ms

Fix 4: Astro 6.1 codec-specific Sharp defaults (saved 18% site-wide)

Before Astro 6.1, tuning image encoding meant setting options on every single <Image /> component. If you wanted MozJPEG compression for all JPEG fallbacks or higher AVIF effort for better compression, you'd repeat yourself across dozens of files.

Astro 6.1 introduced global codec-specific defaults. Configure once, apply everywhere.

The problem

---
// Without global config, you'd repeat this everywhere
import { Image } from 'astro:assets';
import photo from '../assets/photo.jpg';
---

<!-- No way to set MozJPEG or AVIF effort globally before 6.1 -->
<Image src={photo} alt="Team photo" quality={75} />

The fix

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  image: {
    service: {
      config: {
        jpeg: {
          mozjpeg: true,         // Better compression at same quality
        },
        webp: {
          effort: 6,             // Higher effort = smaller files, slower builds
          alphaQuality: 80,      // Quality for transparent WebP
        },
        avif: {
          effort: 4,             // Balance between size and build speed
          chromaSubsampling: '4:2:0',  // Standard subsampling for photos
        },
        png: {
          compressionLevel: 9,   // Maximum PNG compression
        },
      },
    },
  },
});

Each codec option maps directly to Sharp's encoding API. The most impactful settings:

  • jpeg.mozjpeg: true — MozJPEG produces 5-10% smaller JPEGs than the default encoder at the same visual quality. It's slower to encode but since this runs at build time, you don't care.
  • avif.effort: 4 — AVIF effort ranges from 0 (fastest, largest) to 9 (slowest, smallest). Default is 4. If your build times are tolerable, try 6 for an extra 5-8% compression.
  • webp.effort: 6 — Similar tradeoff. Higher effort squeezes more bytes out.

Per-image quality props still take precedence over global defaults, so you can override for specific images that need higher or lower quality.

Results

Across a 40-page site with 120 images:

FormatBefore (avg)After (avg)Saved
JPEG fallbacks89KB76KB-15%
WebP64KB52KB-19%
AVIF48KB39KB-19%
Total image weight7.2MB5.9MB-18%

Build time increased from 28s to 34s — a worthwhile trade for permanently smaller images.

Fix 5: Remote images from your CMS (saved 1.1s LCP)

If your content comes from a headless CMS — Contentful, Sanity, Strapi, or similar — your hero images are probably remote URLs. Astro won't optimize them unless you explicitly allow the domain.

The problem

---
import { Image } from 'astro:assets';
---

<!-- Remote image — Astro renders this WITHOUT optimization -->
<Image
  src="https://cdn.contentful.com/spaces/abc123/hero.jpg"
  alt="Blog hero"
  width={1200}
  height={630}
  loading="eager"
/>

Without the domain in your config, Astro still renders the <img> tag (preserving width/height for CLS prevention), but the image bypasses Sharp entirely. No format conversion, no compression, no resizing. You get the raw file from the CMS CDN.

The fix

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  image: {
    domains: ['cdn.contentful.com', 'images.ctfassets.net'],
    // Or use patterns for more flexibility
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.sanity.io',
      },
    ],
  },
});

Once the domain is authorized, Astro processes remote images through Sharp — converting to WebP/AVIF, compressing, and resizing just like local images.

For SSR sites: caching matters

If you're running Astro in SSR mode (not static), remote images are processed on each request by default. Astro caches processed images in ./node_modules/.astro/ and respects the remote server's Cache-Control headers. Since Astro 5.1, it also uses Last-Modified and ETag headers to avoid re-downloading unchanged images.

Make sure your CMS CDN sends proper cache headers. Most do by default, but it's worth verifying:

curl -sI https://cdn.contentful.com/spaces/abc123/hero.jpg | grep -i cache-control
# Should show something like: cache-control: max-age=31536000

Results

MetricBefore (unoptimized remote)After (optimized)Change
Hero image size1.4MB (JPEG)145KB (AVIF)-90%
LCP3.6s2.5s-1.1s

The full picture

Five fixes, each targeting a different part of the image pipeline:

FixTechniqueLCP Impact
1loading="eager" + fetchpriority="high"-1.4s
2<Picture> with AVIF + WebP-400ms
3Responsive images with layout-600ms (mobile)
4Astro 6.1 codec-specific Sharp defaults-18% image weight
5Authorize remote CMS domains-1.1s

Applied together to our test site:

MetricBeforeAfterChange
LCP (mobile)4.2s1.4s-67%
LCP (desktop)2.8s0.9s-68%
CLS0.140.01-93%
INP45ms42msNo meaningful change
Total image weight7.2MB5.9MB-18%
PageSpeed mobile5296+44 points

The CLS improvement comes from <Image /> and <Picture /> automatically inferring dimensions. The INP was already good — that's Astro's zero-JS advantage.

Image optimization checklist for Astro

Before deploying, verify each of these:

  • LCP image uses loading="eager" and fetchpriority="high"
  • LCP image uses <Picture> with formats={['avif', 'webp']}
  • All other images use <Image /> (not raw <img>)
  • Images are in src/ not public/ (so Sharp can process them)
  • Responsive layout or manual widths/sizes set for hero images
  • Remote CMS domains added to image.domains or image.remotePatterns
  • Astro 6.1+ codec defaults configured in astro.config.mjs
  • No images above the fold using loading="lazy" (the default)

Frequently Asked Questions

Why is my Astro site's LCP slow when Astro is supposed to be fast?

Astro ships zero JavaScript by default, but LCP is usually about images, not JavaScript. If your hero image is unoptimized, uses the default loading="lazy", or loads from an unauthorized remote domain, your LCP will suffer regardless of how little JavaScript you ship. Use Astro's <Image /> component with loading="eager" and fetchpriority="high" on your LCP image.

Should I use the Image component or the Picture component in Astro?

Use <Picture> for your LCP element and any image where you want the browser to choose between AVIF and WebP. <Picture> generates multiple <source> elements so the browser picks the smallest format it supports. Use <Image> for simpler cases where WebP alone is sufficient.

What image format is best for Astro — WebP or AVIF?

AVIF produces 20-30% smaller files than WebP at comparable quality, but takes longer to encode at build time. Use the <Picture> component with formats={['avif', 'webp']} to serve AVIF to browsers that support it and WebP as a fallback. Astro 6.1's codec-specific config lets you tune encoding effort globally to balance file size and build speed.

How do I optimize remote images from a CMS in Astro?

Add your CMS domain to the image.domains array or use image.remotePatterns in astro.config.mjs. Without this, Astro renders remote images without optimization — they bypass Sharp entirely. This is a common gotcha for headless CMS setups where hero images come from external CDNs.

Does Astro's Image component prevent CLS?

Yes. Astro's <Image /> component automatically infers width and height from local images, which reserves space in the layout and prevents Cumulative Layout Shift. For remote images, you must provide width and height manually. Using the responsive layout prop (constrained, full-width, or fixed) also handles sizing automatically.

What's next

If you've optimized your images and LCP is still slow, the problem might be elsewhere — render-blocking CSS, slow server response, or third-party scripts blocking the main thread. For a broader look at Astro performance beyond images, see our guide on Astro's zero-JS architecture and islands.

Need help identifying exactly what's slowing your Astro site down? Run it through PageSpeedFix for a detailed Core Web Vitals analysis with framework-specific fixes.

SpeedBot

A bot on a mission to make every website on the internet faster. Writes guides at PageSpeedFix because slow pages shouldn't exist.