Type something to search...
How to optimize images with the Astro Image component?

How to optimize images with the Astro Image component?

To optimize images with the Astro Image component, put your image files in src/ (for example src/assets/), import them into a component, and render them with <Image /> from astro:assets. Astro reads the image's real dimensions, converts it to WebP by default, resizes it to the width you ask for, and outputs an <img> tag with width, height, loading="lazy" and decoding="async" already set. Add priority for the main image above the fold, widths and sizes or a layout for responsive srcset output, and format or quality to fine-tune file size. The only required props are src and alt.

Images are usually the heaviest part of a page, and the most common causes of slow loading and layout shift. Handling them by hand means exporting several sizes, converting formats and remembering dimensions on every tag. The Image component automates all of that at build time. In this article you'll learn the difference between images in src/ and public/, the props you'll use most, how priority changes loading, how to generate responsive images, how to allow and optimize remote images, how to use images in Markdown and content collections, and when to reach for getImage().

The Basic Usage

---
// src/pages/index.astro
import { Image } from 'astro:assets';
import hero from '../assets/hero.jpg';
---
<Image src={hero} alt="Sunrise over a gradient sky" />

Importing hero.jpg gives you an ImageMetadata object with the file's path, width, height and format. Passing it to <Image /> produces this HTML after astro build:

<img src="/_astro/hero.BXpjeiMQ_ZDdpFL.webp" alt="Sunrise over a gradient sky" loading="lazy" decoding="async" width="2400" height="1350">

Several things happened without any configuration:

  • Format conversion: the JPEG became WebP, the default output format.
  • Dimensions: width and height were read from the file, so the browser can reserve space and avoid layout shift.
  • Lazy loading: loading="lazy" delays the download until the image is near the viewport.
  • Async decoding: decoding="async" lets the browser decode off the main thread.
  • Cache-friendly file names: the hashed name changes when the image changes, so it can be cached for a long time.

For the 2400px test image, the 52.9 KB JPEG became a 15.2 KB WebP at full size. Real photos vary, but savings of that order are common.

Images Must Have Alt Text

alt is required. Leaving it out fails the build:

[ImageMissingAlt] Image missing "alt" property. "alt" text is required to describe important images on the page.

For purely decorative images, pass alt="" so screen readers skip them.

src/ vs public/

Where you keep an image decides what Astro can do with it:

LocationHow you reference itOptimized?Dimensions
src/ (e.g. src/assets/)import hero from '../assets/hero.jpg'YesInferred
public/src="/images/banner.jpg"NoYou must set them
Remote URLsrc="https://..."Only if allowed in configYou must set them, or use inferSize

Files in public/ are copied to the output untouched. <Image /> still renders them, and still adds lazy loading, but it won't resize or convert them. You also have to provide width and height; otherwise the build stops:

[MissingImageDimension] Missing width and height attributes for /images/banner.jpg. When using remote images, both dimensions are required in order to avoid CLS.
  Hint:
    If your image is inside your `src` folder, you probably meant to import it instead.

The rule of thumb: keep content images in src/ so they get optimized, and use public/ only for files that must keep an exact URL, such as favicons or images referenced by external services.

Resizing with width and height

Give <Image /> a width and it resizes the file, keeping the aspect ratio:

<Image src={hero} alt="Sunrise" width={800} />
<img src="/_astro/hero.BXpjeiMQ_Z1Nu4Vs.webp" alt="Sunrise" loading="lazy" decoding="async" width="800" height="450">

Astro calculated the height of 450 from the original 16:9 ratio, and the file dropped from 15.2 KB to 3.6 KB. Set both width and height only when you want a different aspect ratio; the image is then cropped, using the fit and position props (which default to cover and center).

Always resize to roughly the largest size the image is displayed at. Sending a 2400px image into an 800px column wastes bandwidth even if it's WebP.

Loading the Most Important Image First

Lazy loading is the right default, but it's wrong for the image visitors see first, usually the hero or the post's cover. Delaying that one hurts Largest Contentful Paint. Add priority:

<Image src={hero} alt="Hero" width={1200} priority />
<img src="/_astro/hero.BXpjeiMQ_ZFTsfW.webp" alt="Hero" loading="eager" decoding="sync" fetchpriority="high" width="1200" height="675">

priority sets loading="eager", decoding="sync" and fetchpriority="high" in one go. Use it on one, or at most a couple of, images per page. If everything is high priority, nothing is. The post on lazy loading images and components in Astro goes deeper on choosing what to load first.

Controlling Format and Quality

format picks the output type, and quality trades size for fidelity:

<Image src={team} alt="Team" width={600} format="avif" quality="mid" />
<img src="/_astro/team.D9XbmwcD_ebB9m.avif" alt="Team" loading="lazy" decoding="async" width="600" height="338">

quality accepts the presets low, mid, high and max, or a number from 0 to 100. AVIF is often smaller than WebP for photos, but encoding is slower and very old browsers don't support it. If you want to serve AVIF with a WebP or JPEG fallback, use the <Picture /> component instead, which outputs several formats. See using the Picture component for responsive images.

Responsive Images with srcset

One file can't be ideal for every screen. <Image /> can generate a srcset in three ways.

densities for Fixed-Size Images

For an image that's always the same CSS size, such as an avatar or a logo, densities creates versions for high-DPI screens:

<Image src={team} alt="Team" width={400} densities={[1.5, 2]} />
<img src="/_astro/team.D9XbmwcD_cBde0.webp" srcset="/_astro/team.D9XbmwcD_xBt6O.webp 1.5x, /_astro/team.D9XbmwcD_Zpt0cM.webp 2x" alt="Team" loading="lazy" decoding="async" width="400" height="225">

widths and sizes for Fluid Images

For an image whose size changes with the layout, list the widths to generate and describe how wide it's displayed with sizes:

<Image
  src={hero}
  alt="Hero"
  widths={[480, 960, 1600]}
  sizes="(max-width: 600px) 480px, 960px"
/>
<img src="/_astro/hero.BXpjeiMQ_ZDdpFL.webp" srcset="/_astro/hero.BXpjeiMQ_1m3P59.webp 480w, /_astro/hero.BXpjeiMQ_Z1yAchw.webp 960w, /_astro/hero.BXpjeiMQ_Z29CTki.webp 1600w" alt="Hero" sizes="(max-width: 600px) 480px, 960px" loading="lazy" decoding="async" width="2400" height="1350">

The browser combines sizes with the screen's width and pixel density to choose the smallest file that still looks sharp. Don't combine densities with widths; pick one.

layout for Automatic srcset

Since Astro 5.10, the layout prop works out srcset and sizes for you. It takes constrained, full-width, fixed or none:

<Image src={hero} alt="Hero" width={1200} height={675} layout="constrained" />
<img src="/_astro/hero.BXpjeiMQ_Z2cHRKx.webp" srcset="... 640w, ... 750w, ... 828w, ... 1080w, ... 1200w, ... 1280w, ... 1668w, ... 2048w, ... 2400w" alt="Hero" loading="lazy" decoding="async" sizes="(min-width: 1200px) 1200px, 100vw" data-astro-image="constrained" data-astro-image-fit="cover" data-astro-image-pos="center" width="1200" height="675">
  • constrained: shrinks to fit its container but never displays wider than the given width. The best default for content images.
  • full-width: always fills the container, for banners and hero images.
  • fixed: keeps the exact size, with a density-based srcset.
  • none: no responsive behaviour.

constrained still generated files larger than 1200px, so a 1200px slot on a 2x screen gets a sharp image. To apply a layout to every image, including Markdown images, set it globally:

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

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

responsiveStyles adds a small set of global styles (using :where(), so they have zero specificity) that make the layouts behave as described. It's off by default. Leave it off if you use Tailwind 4, whose cascade layers would otherwise be overridden by Astro's styles, and style images with utilities instead.

Remote Images

For images hosted elsewhere, pass the full URL with width and height:

<Image src="https://picsum.photos/id/10/1600/900" alt="Remote" width={800} height={450} />

By default Astro doesn't download or transform remote images; it renders the <img> tag with your dimensions to prevent layout shift. To have Astro optimize them, allow the source in your config with image.domains or the more flexible image.remotePatterns:

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

export default defineConfig({
  image: {
    domains: ['picsum.photos'],
    remotePatterns: [{ protocol: 'https', hostname: '**.picsum.photos' }],
  },
});

Testing this turned up a subtle point. With only domains: ['picsum.photos'], the build warned that the image redirected to fastly.picsum.photos, which wasn't an allowed location, and skipped optimization. Allowing the CDN's subdomains with remotePatterns fixed it, and the output became a local WebP:

<img src="/_astro/900_Z1ts8fk.webp" alt="Remote" loading="lazy" decoding="async" width="800" height="450">

If you don't know a remote image's size, the inferSize prop (or the inferRemoteSize() function) fetches the dimensions for you, but only for allowed domains. It adds a network request per image at build time, or per request for on-demand pages, and it depends on the remote server cooperating; against the test image host it failed with FailedToFetchRemoteImageDimensions. Prefer explicit dimensions whenever you have them.

Images in Markdown and Content Collections

Relative image paths in Markdown are optimized automatically:

![A gradient sunrise](./cover.jpg)

In content collections, the image() helper in your schema validates that the file exists and turns the frontmatter path into the same ImageMetadata you'd get from an import:

// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      cover: image(),
      coverAlt: z.string(),
    }),
});

export const collections = { blog };
---
// src/pages/posts/[id].astro
import { Image } from 'astro:assets';
import { getCollection, render } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({ params: { id: post.id }, props: { post } }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---
<article>
  <Image src={post.data.cover} alt={post.data.coverAlt} width={1200} priority />
  <h1>{post.data.title}</h1>
  <Content />
</article>

The cover rendered with fetchpriority="high" at 1200px, and the Markdown image inside <Content /> came out as a lazy-loaded WebP. Paths in frontmatter are relative to the entry file. More on schemas in defining a content collection schema with Zod.

getImage() for Everything Else

When you need an optimized image URL rather than an <img> tag, such as for a CSS background, an Open Graph meta tag or an API response, use getImage():

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

const og = await getImage({ src: hero, width: 1200, height: 630, format: 'jpg' });
---
<meta property="og:image" content={new URL(og.src, Astro.site)} />

Astro.site comes from the site option in your config, which you need for absolute URLs like this. getImage() accepts the same options as <Image /> except alt and sizes, and returns src, attributes and srcSet. It only runs on the server and throws if you call it in client-side code.

Image Services

Astro uses Sharp to process images by default. If your host doesn't support Sharp, use the passthrough service, which keeps the component's benefits (required alt, dimensions, lazy loading) without transforming files:

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

export default defineConfig({
  image: { service: passthroughImageService() },
});

Processed images are cached in node_modules/.astro, so repeat builds only process new or changed images. Keep that folder in your CI cache to speed up deploys.


FAQ: The Astro Image Component

It's probably in public/ or on a remote domain you haven't allowed. Move local images into src/ and import them, or add remote hosts to image.domains or image.remotePatterns, including any CDN host the URL redirects to.

WebP by default. Change it with the format prop, for example format avif, or use the Picture component to output several formats with a fallback.

Not for images imported from src/, because Astro reads them from the file. Images from public/ and remote URLs need both, or inferSize for allowed remote domains.

Add the priority prop. It sets loading eager, decoding sync and fetchpriority high, which helps the browser fetch the image as early as possible.

Use densities for images that are always the same CSS size, widths with sizes when you know exactly how the image is displayed, and layout constrained or full-width when you want Astro to work out srcset and sizes for you.

Not in plain .md files, but standard Markdown image syntax with a relative path is optimized automatically. In MDX you can import and use Image and Picture directly.


Conclusion

The Astro Image component turns a plain import into an optimized, correctly sized and lazy-loaded image. Keep images in src/, always write meaningful alt text, set width to the size you actually display, and add priority to the one image that matters most for first paint. Astro handles the WebP conversion, dimensions and cache-busting file names for you.

For responsive images, choose densities, widths with sizes, or a layout, and allow remote hosts explicitly when you want them optimized too. With image() in content collections and getImage() for everything that isn't an <img> tag, the same pipeline covers every image on your site.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper