Astro Image and Picture Components
Astro’s image story is deliberately compile-first: the astro:assets module treats every local image as a typed module import, hands it to an image service (Sharp by default) during the build, and emits hashed, correctly-sized derivatives with the intrinsic width and height baked into the markup so layout shift is impossible by construction. This page is part of Framework & Build-Tool Media Integration and covers the whole surface: the <Image> and <Picture> components, the built-in Sharp service (plus the squoosh and passthrough alternatives), the image configuration block that authorises remote hosts, the difference between densities and widths, the getImage() escape hatch for non-<img> contexts, and how content-collection images differ from ones dropped in src/assets. It sits alongside the Nuxt and Vite image asset pipeline as the third major build-time model in this section.
Concept & architecture
The image service abstraction
Everything in astro:assets routes through an image service — a small adapter with two jobs: compute the transform URL/parameters, and (for local images) perform the actual encode. Astro ships three:
- Sharp (default) — libvips-backed, fast, produces AVIF and WebP. This is what you want in almost every case.
squoosh— a WebAssembly encoder used historically when Sharp could not install in a constrained environment. Slower and lower-quality AVIF; retained for compatibility.passthrough/noop— performs no optimisation. Used when a downstream host (Netlify, Vercel image CDN) owns the transform, so Astro just emits the original plus the correct markup.
For a local image the service runs at build time and writes files into dist/_astro/. For a remote image, Astro cannot read the bytes at build time in the general case, so the component emits a service URL and the transform happens on demand (via an adapter’s image endpoint) — which is exactly why remote hosts must be explicitly authorised before Astro will optimise them.
<Image> vs <Picture>
<Image> renders exactly one <img>. It optimises to a single output format (WebP by default) and produces a srcset when you pass widths or densities. Use it when one format is enough — typically WebP, which every modern browser decodes.
<Picture> renders a <picture> with one <source> per entry in its formats prop, plus a fallback <img>. Use it when you want an AVIF-first cascade with a WebP and original fallback — the multi-format art-direction case. The formats={['avif','webp']} cascade and its Safari edge cases are covered in depth in Astro Picture component for AVIF and WebP.
Configuration reference
| Setting / prop | Where | Meaning |
|---|---|---|
image.service |
astro.config.mjs |
Selects the backend: sharp (default), squoosh (legacy WASM), or a passthrough service. |
image.domains |
astro.config.mjs |
Exact remote hostnames Astro is allowed to optimise (e.g. ['images.unsplash.com']). |
image.remotePatterns |
astro.config.mjs |
Pattern-based remote allowlist (protocol, hostname with wildcards) for many origins. |
widths |
<Image>/<Picture> |
Explicit pixel widths for the srcset; pair with sizes. Best for fluid layouts. |
densities |
<Image>/<Picture> |
DPR multipliers ([1, 2]) generating a 1x/2x srcset. Best for fixed-size images. |
sizes |
<Image>/<Picture> |
The layout-width hint the browser uses to choose a widths candidate. Required with widths. |
format |
<Image> |
Single output format for <Image> (avif, webp, png, jpg). |
formats |
<Picture> |
Ordered list of <source> formats (['avif','webp']), smallest first. |
quality |
both | Encoder quality: a number, or a named preset (low, mid, high, max). |
priority |
both | Sets loading="eager", decoding="sync", and fetchpriority="high" together for the LCP image. |
getImage() |
astro:assets |
Programmatic API returning { src, srcSet, attributes } for non-<img> uses (CSS backgrounds, <link rel=preload>). |
Tradeoff: densities and widths are mutually exclusive on a single component. densities={[1, 2]} produces exactly 1x/2x candidates sized off the base width — perfect for an avatar or icon with a fixed CSS box. widths={[400, 800, 1200]} produces width-descriptor candidates the browser matches against sizes — correct for a fluid image that spans different fractions of the viewport. Mixing them is a config error; choose based on whether the rendered size is fixed or fluid.
Step-by-step
Step 1 — Configure the image service and remote allowlist
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
image: {
// service selects the encoder. Sharp is the default and rarely needs
// changing; passing squoosh explicitly is only for environments where
// Sharp's native binary cannot install.
service: { entrypoint: 'astro/assets/services/sharp' },
// domains authorises EXACT remote hostnames. A remote <Image src="https://…">
// whose host is not listed here (or in remotePatterns) is emitted UNOPTIMISED
// — Astro will not fetch and re-encode an arbitrary origin for you.
domains: ['images.unsplash.com'],
// remotePatterns authorises hosts by pattern — use for a whole CDN.
// protocol/hostname wildcards avoid listing every subdomain by hand.
remotePatterns: [{ protocol: 'https', hostname: '**.cdn.example.com' }],
},
});
Step 2 — Import and render a local image with <Image>
---
// component.astro — frontmatter (build-time) section
import { Image } from 'astro:assets';
// Local images are IMPORTED, not referenced by string. The import is a typed
// object carrying intrinsic width/height/format — Astro uses it to set the
// <img> dimensions automatically, so no layout shift is possible.
import hero from '../assets/hero.jpg';
---
<!--
widths + sizes = a responsive srcset for a fluid image.
format="webp" emits a single WebP <img> (broad support, no fallback needed).
Astro fills width/height from the import; you may override to change the box.
-->
<Image
src={hero}
alt="Studio product hero"
widths={[400, 800, 1200]}
sizes="(max-width: 800px) 100vw, 1200px"
format="webp"
quality="mid"
/>
Step 3 — Render a fixed-size image with densities
---
import { Image } from 'astro:assets';
import avatar from '../assets/avatar.png';
---
<!--
densities is the right tool for a fixed-size image: the CSS box is 96px,
so we only need 1x and 2x candidates. width sets the base; densities
multiplies it. Do NOT also pass widths — the two are mutually exclusive.
-->
<Image
src={avatar}
alt="Author avatar"
width={96}
height={96}
densities={[1, 2]}
format="webp"
/>
Step 4 — Handle a remote image
---
import { Image } from 'astro:assets';
---
<!--
Remote images are passed as a STRING src. width and height are REQUIRED
here (Astro cannot read the file to infer them at build time). The host
MUST appear in image.domains or image.remotePatterns or the image is
served unoptimised at its original bytes.
-->
<Image
src="https://images.unsplash.com/photo-123"
alt="Editorial landscape"
width={1200}
height={800}
inferSize={false}
format="webp"
/>
Step 5 — Use getImage() for non-<img> contexts
---
// getImage() returns the same optimisation result as <Image>, but as data
// you place yourself — e.g. a CSS background or a preload hint. It does NOT
// render an element, so you own the markup.
import { getImage } from 'astro:assets';
import bg from '../assets/texture.jpg';
const optimized = await getImage({
src: bg,
format: 'avif',
width: 1600,
});
// optimized.src → "/_astro/texture.<hash>.avif"
// optimized.srcSet.attribute → the srcset string
// optimized.attributes → { width, height, ... }
---
<!-- Preload the LCP background so it is fetched before CSS discovers it. -->
<link rel="preload" as="image" type="image/avif" href={optimized.src} />
<div style={`background-image:url(${optimized.src})`} class="hero-bg"></div>
Tradeoff: content-collection images (declared with the image() schema helper in src/content/config.ts) behave like src/assets imports — they are validated and optimised at build time. But an image referenced only by a string path in frontmatter (not through the image() schema) is treated as a plain public asset and is not optimised. If collection images ship at full size, the schema is using z.string() where it should use image().
Content collections and typed image fields
Content collections are where Astro’s build-time model earns its keep — and where the most common “why isn’t my image optimised” bug lives. A collection entry (a Markdown or MDX file, or a data entry) can reference an image in its frontmatter, but Astro only optimises it if the schema declares that field with the image() helper rather than a plain string.
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
// The image() helper is injected into the schema context. It resolves the
// frontmatter path RELATIVE TO THE ENTRY FILE and returns the same typed
// image object a normal import would — with intrinsic width/height — so
// <Image>/<Picture> can optimise it.
schema: ({ image }) =>
z.object({
title: z.string(),
// cover uses image(): the referenced file is validated at build time
// (a missing file fails the build) and becomes an optimisable asset.
cover: image(),
// A plain z.string() here would NOT be optimised — Astro would treat
// the value as an opaque URL string and ship the original bytes.
externalCover: z.string().url().optional(),
}),
});
export const collections = { blog };
---
// A page rendering a collection entry's cover image.
import { getCollection } from 'astro:content';
import { Picture } from 'astro:assets';
const posts = await getCollection('blog');
const post = posts[0];
---
<!--
post.data.cover is a typed image object (because the schema used image()),
so <Picture> optimises it exactly like a src/assets import — no special
handling. If cover were a z.string(), this would render the raw file.
-->
<Picture
src={post.data.cover}
formats={['avif', 'webp']}
widths={[400, 800, 1200]}
sizes="(max-width: 800px) 100vw, 1200px"
alt={post.data.title}
/>
Tradeoff: the image() schema helper validates paths at build time — a typo in a post’s frontmatter fails the whole build rather than silently rendering a broken image in production. That strictness is the point: it moves a class of content error left, into CI. The cost is that authors must keep referenced images inside the project tree; a frontmatter path pointing at a URL belongs in a separate z.string().url() field and is handled as a remote image instead.
Build-time versus on-demand: SSR and the passthrough service
Everything above assumes a static build (astro build to HTML). Astro also supports server output via an adapter, and the image story changes subtly in that mode. With an SSR adapter installed, Astro exposes an image endpoint (/_image) that can perform transforms on demand — which is what makes remote-image optimisation and runtime resizing possible for content that did not exist at build time.
// astro.config.mjs — server output with an adapter enables the /_image endpoint
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
image: {
service: { entrypoint: 'astro/assets/services/sharp' },
// With SSR, an authorised remote image is transformed on first request by
// the /_image endpoint and can be cached downstream by your CDN — set the
// same immutable Cache-Control you would use for static hashed assets.
domains: ['images.unsplash.com'],
},
});
Warning: running the Sharp service on the /_image endpoint means the first request for a given size pays the encode cost synchronously, inflating TTFB for that request. Put a CDN in front and cache the endpoint responses aggressively, or the origin re-encodes on every cold cache miss. For a purely static site you avoid this entirely — every derivative is materialised in CI — which is the stronger default whenever your image set is known at build time.
When a downstream platform owns image optimisation (Netlify Image CDN, Vercel), select a passthrough service so Astro emits the original plus correct markup and defers the actual transform to the platform. This avoids double-encoding: you do not want Sharp producing an AVIF that the platform then re-transforms.
Parameter reference
| Prop | Component | Notes |
|---|---|---|
src |
both | An imported module (local) or a string URL (remote). Local imports carry intrinsic dimensions. |
alt |
both | Required — Astro throws a build error if omitted, enforcing accessibility. |
widths |
both | Width-descriptor srcset; requires sizes. |
densities |
both | DPR srcset; mutually exclusive with widths. |
formats |
<Picture> |
Ordered <source> formats, e.g. ['avif','webp']. |
format |
<Image> |
Single output format. |
quality |
both | Number or preset name (low/mid/high/max). |
priority |
both | Bundles eager + sync decode + fetchpriority="high" for the LCP image. |
inferSize |
remote <Image> |
true lets Astro fetch a remote image’s dimensions at build (extra request); default requires explicit width/height. |
Tradeoffs & failure modes
| Failure mode | Cause | Fix |
|---|---|---|
<Image> throws “alt is required” at build |
Missing alt prop |
Add alt; use alt="" for decorative images |
| Remote image ships full-size | Host not in domains/remotePatterns |
Add the origin to the allowlist |
Build errors on missing width/height for remote |
Astro cannot infer remote dimensions | Pass explicit width+height, or set inferSize={true} |
densities and widths both set |
They are mutually exclusive | Keep one: densities for fixed size, widths for fluid |
| Collection images not optimised | Schema uses z.string() for the image field |
Use the image() schema helper so imports are typed and optimised |
AVIF-only <Image> breaks older Safari |
Single-format <img> with no fallback |
Use <Picture formats={['avif','webp']}> for a cascade |
| Slow builds on a large gallery | Sharp encoding many widths × formats | Trim widths to real breakpoints; lower quality; the encode is build-time only |
Warning: <Image> and <Picture> optimise local images at build time only — there is no runtime resize for a statically-built Astro site. If your images are user-generated or arrive after deploy, you need either an SSR adapter with an image endpoint, a passthrough service that defers to a CDN, or an external image service. Do not expect a static astro build to resize an image that did not exist at build time.
Debugging & validation
Inspect the emitted markup and files
# After `astro build`, confirm derivatives landed in dist/_astro with hashes.
find dist/_astro -type f \( -name '*.avif' -o -name '*.webp' \) -printf '%f %s bytes\n' | sort
# Extract the generated <img>/<picture> from a built page to confirm the
# srcset, intrinsic width/height, and format order are what you configured.
grep -oE '<(picture|source|img)[^>]*>' dist/index.html | head -n 20
Confirm remote authorisation is working
# A remote image that was optimised will have a /_astro/ URL in the markup.
# If the original remote URL appears verbatim, the host was NOT authorised
# and the image was passed through unoptimised — add it to image.domains.
grep -oE 'src="[^"]*"' dist/index.html | grep -E 'unsplash|_astro'
Tradeoff: because Astro bakes width/height into every <Image>, it eliminates the Cumulative Layout Shift class of bug that plagues hand-written <img> tags — but it also means a wrong intrinsic ratio (e.g. overriding width without height) produces a distorted image rather than a shifted layout. When overriding dimensions, always set both, and keep the aspect ratio matching the source. Pair this with the CDN caching rules in best practices for setting max-age on CDN media assets so the hashed _astro files are served immutable.
Related
- Astro Picture Component for AVIF and WebP — the exact multi-format
<Picture>cascade with fallbacks and verification - Nuxt and Vite Image Asset Pipeline — the parallel build-time model using
@nuxt/imageand vite-imagetools - Next.js Image Component Optimization — a runtime-optimising component contrast to Astro’s build-time approach
- AVIF vs WebP Compression Benchmarks — the data behind ordering
formats={['avif','webp']} - Art direction with the HTML picture element — what
<Picture>compiles down to, and when to hand-author it - Framework & Build-Tool Media Integration — parent section on image handling across frameworks and bundlers