Overview

Astro’s built-in image optimization runs through astro:assets. The <Image /> component transforms local images at build time: it generates AVIF and WebP variants, sets explicit width and height to prevent layout shift, and writes a responsive srcset when you pass widths. No external service is needed for local images. For remote images, you must allowlist the hostname; Astro fetches and optimizes them at build time (static) or request time (SSR).

Use <Image /> for local images with a single output size

Import the image file into the component and pass it to src. Astro infers width and height from the source file and generates the optimized output format.

---
import { Image } from "astro:assets";
import hero from "../assets/hero.jpg";
---
<Image src={hero} alt="A description of the image" />

By default, Astro converts to WebP. Pass format="avif" to force AVIF output. AVIF gives 30 to 50 percent smaller files than WebP at the same quality but takes longer to encode; use it for production builds.

Use <Picture /> for responsive images with multiple sizes

<Picture /> generates a <picture> element with <source> tags for each format and a <img> fallback. Pass widths and sizes to control the responsive behavior.

---
import { Picture } from "astro:assets";
import hero from "../assets/hero.jpg";
---
<Picture
  src={hero}
  widths={[400, 800, 1200]}
  sizes="(max-width: 720px) 100vw, 800px"
  formats={["avif", "webp"]}
  alt="Hero image"
/>

The browser receives AVIF for browsers that support it, WebP for browsers that do not, and the original JPEG as a final fallback. Always pass both widths and sizes together; without sizes, the browser assumes the image is 100vw.

Allowlist remote image domains in astro.config.mjs

Remote images require explicit domain allowlisting. Add the image.domains array to your config.

// astro.config.mjs
export default defineConfig({
  image: {
    domains: ["images.contentfulassets.com", "cdn.sanity.io"],
  },
});

For broader patterns, use image.remotePatterns with a hostname and optional protocol and pathname glob. Astro rejects remote image URLs not covered by domains or remotePatterns at build time.

Set the LCP image to eager loading with high fetch priority

The Largest Contentful Paint image should not be lazy-loaded. Pass loading="eager" and fetchpriority="high" to the LCP image.

<Image
  src={hero}
  alt="Hero"
  loading="eager"
  fetchpriority="high"
/>

All other images below the fold default to loading="lazy". Astro does not set loading automatically; you must pass it explicitly when you want eager loading.

Store source images in src/assets/, not public/

Images in public/ are copied verbatim to the build output. Astro does not transform them. Put source photos, illustrations, and diagrams in src/assets/ so the optimizer runs. Use public/ for pre-sized assets: favicons, OG images already at 1200x630, and images referenced by external URLs.

A common mistake is placing a large hero JPEG in public/ and wondering why Lighthouse reports a 3 MB image. Move it to src/assets/ and import it with <Image />.

Astro Image vs Next/Image

Both optimize images at build or request time, generate srcset, and set dimensions. The differences:

  • Astro works at build time for static output; no server is needed. Next.js <Image> optimizes at request time via an API route.
  • Astro <Picture /> exposes format selection directly. Next.js uses next.config.js images.formats globally.
  • Next.js <Image> has a placeholder="blur" prop that generates a base64 LQIP automatically. Astro does not have a built-in LQIP; generate the placeholder with a build script or a Vite plugin and pass it as style.

For LQIP in Astro, generate a base64 string during the build and inline it:

<Image
  src={hero}
  alt="Hero"
  style={`background-image: url('${lqipBase64}'); background-size: cover;`}
/>