Images & Assets
Images are usually the heaviest payload a page ships, so how a framework handles them directly shapes Core Web Vitals and perceived speed. Astro treats images as first-class assets: it processes them through a built-in pipeline that resizes, re-encodes, and optimizes files at build time (or on demand), all while keeping its zero-JS-by-default philosophy intact. Instead of shipping a 4 MB original and hoping the browser copes, Astro generates lean, correctly sized files and the right markup to load them. This page gives you the mental model for the whole assets system before you dive into the specific tools.
How the assets pipeline works
Astro’s image handling is built on Sharp, a fast native image processor enabled by default in Astro 4 and 5. When you reference an image through the pipeline, Astro reads the source file, derives its intrinsic dimensions, and emits optimized variants into your build output. Because dimensions are known ahead of time, Astro can stamp width and height (or an aspect-ratio) onto the markup automatically, which eliminates layout shift.
There are two distinct categories of images, and they behave differently:
| Source | Where it lives | Optimized? | How you reference it |
|---|---|---|---|
| Local, imported | src/ (e.g. src/assets/) | Yes, full pipeline | import then pass to <Image /> |
| Local, public | public/ | No, served as-is | Plain string path like /logo.png |
| Remote | Another domain / CMS | Yes, if authorized | URL string passed to <Image /> |
Files in src/ are part of the build graph: Astro can hash their filenames for caching, optimize them, and fail the build if a path is wrong. Files in public/ are copied verbatim with no processing, which is the right choice for assets that must keep an exact filename (favicons, robots.txt, OG images referenced by absolute URL).
Prefer
src/assets/overpublic/for content imagery. The pipeline only optimizes what it can see in the module graph, and importing gives you build-time guarantees that a typo’d path never silently 404s in production.
Importing and using a local image
The recommended path is the built-in <Image /> component from astro:assets. You import the source as an ES module, which yields a metadata object (with src, width, height, and format), then hand that object to the component.
---
import { Image } from "astro:assets";
import hero from "../assets/hero.jpg";
---
<Image src={hero} alt="Mountain range at sunrise" width={800} quality={80} />
At build time this produces an optimized <img> with explicit dimensions and, by default, modern formats. You only need to specify one of width/height when downscaling — the other is inferred from the source’s aspect ratio.
Automatic format conversion
Astro defaults to converting images to efficient formats. WebP and AVIF typically cut file size by 25-50% versus JPEG/PNG at equivalent visual quality. You control the target with the format and quality props.
---
import { Image } from "astro:assets";
import photo from "../assets/photo.png";
---
<Image src={photo} alt="Product shot" format="avif" quality="high" />
quality accepts either a number (0–100) or a named preset: low, mid, high, or max. Named presets let Astro pick sensible per-format defaults rather than forcing one number across codecs that compress very differently.
Responsive sizing
For images that need to adapt to viewport or container width, Astro can generate multiple resolutions and emit a proper srcset/sizes pair. The simplest switch is enabling responsive layout globally in astro.config.mjs:
import { defineConfig } from "astro/config";
export default defineConfig({
image: {
responsiveStyles: true,
layout: "constrained",
},
});
With layout set, the <Image /> (and the <Picture />) component automatically produces a range of widths and the CSS to scale them. You can still override per-image with the widths and sizes props for fine control. The dedicated responsive images page covers the layout modes in depth.
Local vs. public assets in practice
When an asset truly must not change — a manifest.json icon, a PDF download, or a sitemap-referenced share image — drop it in public/ and reference it by its literal path:
<a href="/downloads/spec.pdf">Download the spec (PDF)</a>
<img src="/favicon-fallback.png" alt="" width="32" height="32" />
These bypass the pipeline entirely. For everything visual and content-driven, keep it in src/ so Astro can optimize and fingerprint it.
Verifying the output
After a build you can confirm the pipeline ran by inspecting the emitted assets directory.
npm run build
ls dist/_astro
Output:
hero.Bz9kQ1mD_2pXwY.webp
photo.Df3aL0nE_1QrTz.avif
The hashed filenames are content-derived, so a deployed asset can be cached aggressively (immutable) without risking stale files after an update.
Best Practices
- Store content images in
src/assets/and import them; reservepublic/for files that need a fixed, unhashed name. - Always supply a meaningful
alt; usealt=""for purely decorative images so screen readers skip them. - Let Astro infer dimensions from imports to prevent cumulative layout shift instead of hardcoding mismatched values.
- Reach for
<Picture />when you need multiple formats with art direction; use<Image />for the common single-source case. - Prefer named
qualitypresets (high,mid) over magic numbers so each output format gets an appropriate default. - Authorize remote domains explicitly in
image.domains/image.remotePatternsrather than disabling optimization for external URLs.