Astro Picture component for AVIF and WebP
A single-format <Image> is fine when WebP alone covers your audience, but the moment you want the smaller AVIF with a safe fallback for browsers that lack it, you need Astro’s <Picture> — it emits a real <picture> element with one <source> per format and a fallback <img>, all with intrinsic dimensions baked in at build time. This guide is part of the Astro Image and Picture Components section within Framework & Build-Tool Media Integration, and shows the exact <Picture> invocation for an AVIF-first, WebP-second, JPEG-fallback cascade — with responsive widths, honest sizes, priority for the LCP case, and the verification steps that prove the encoder emitted what you asked for.
Prerequisite checklist
How the cascade resolves in the browser
<Picture formats={['avif','webp']}> emits <source type="image/avif"> first, <source type="image/webp"> second, and a fallback <img> (JPEG/PNG by default) last. The browser walks the sources top-to-bottom and takes the first type it can decode: AVIF for Chrome/Firefox and Safari 16+, WebP for Safari 14–15, and the JPEG <img> for anything older. Because the file for each width already exists on disk after the build, no runtime negotiation or server logic is involved — the browser simply picks a URL. The precise Safari version boundary and why AVIF-only markup breaks Safari 15 is detailed in how to configure AVIF fallbacks for Safari 14.
Exact solution
Step 1 — Author the <Picture> element
---
// hero.astro — build-time frontmatter
import { Picture } from 'astro:assets';
// Local import → typed object with intrinsic width/height. Astro reads these
// to set the fallback <img> dimensions automatically, so CLS is impossible.
import hero from '../assets/hero.jpg';
---
<!--
formats order = <source> order. AVIF first so capable browsers get the
smallest file; WebP second for Safari 14–15; the fallback <img> is JPEG.
fallbackFormat controls that <img>'s format (defaults to the source's own,
jpg here) — keep it a universally-decodable format.
widths generates a responsive srcset for EACH format. sizes tells the
browser the layout width so it can pick the right candidate — it must be
honest about how much of the viewport the image really occupies.
alt is REQUIRED; Astro fails the build without it.
-->
<Picture
src={hero}
formats={['avif', 'webp']}
fallbackFormat="jpg"
widths={[400, 800, 1200, 1600]}
sizes="(max-width: 800px) 100vw, (max-width: 1400px) 66vw, 1200px"
alt="Studio product hero on a neutral background"
quality="mid"
/>
Step 2 — Mark the above-the-fold image as the LCP candidate
---
import { Picture } from 'astro:assets';
import hero from '../assets/hero.jpg';
---
<!--
priority bundles loading="eager", decoding="sync", and fetchpriority="high"
onto the fallback <img>. Use it on EXACTLY ONE image per page — the LCP
candidate. Applying it to several images floods the high-priority fetch
queue and starves CSS/fonts. Omit priority on below-fold Pictures so they
default to loading="lazy".
-->
<Picture
src={hero}
formats={['avif', 'webp']}
fallbackFormat="jpg"
widths={[400, 800, 1200, 1600]}
sizes="(max-width: 800px) 100vw, 1200px"
alt="Studio product hero on a neutral background"
priority
/>
Step 3 — The markup Astro emits
<!-- Approximate output of the <Picture> above (hashes abbreviated): -->
<picture>
<source
type="image/avif"
srcset="/_astro/hero.a1.avif 400w, /_astro/hero.b2.avif 800w, /_astro/hero.c3.avif 1200w, /_astro/hero.d4.avif 1600w"
sizes="(max-width: 800px) 100vw, 1200px">
<source
type="image/webp"
srcset="/_astro/hero.e5.webp 400w, /_astro/hero.f6.webp 800w, /_astro/hero.g7.webp 1200w, /_astro/hero.h8.webp 1600w"
sizes="(max-width: 800px) 100vw, 1200px">
<!-- width/height come from the import; they set the box and prevent CLS. -->
<img
src="/_astro/hero.i9.jpg"
alt="Studio product hero on a neutral background"
width="1600" height="900"
loading="eager" decoding="sync" fetchpriority="high">
</picture>
Verification
1. Confirm the source order and format types
# Extract the generated <picture> from the built page. The FIRST <source>
# must be type="image/avif"; if webp appears first, the formats array order
# is wrong and AVIF-capable browsers will waste bytes on WebP.
grep -oE '<(picture|source|img)[^>]*>' dist/index.html | head -n 12
2. Confirm every width was emitted per format
# Four widths × two next-gen formats = eight derivatives, plus one jpg fallback.
find dist/_astro -name 'hero.*' \( -name '*.avif' -o -name '*.webp' -o -name '*.jpg' \) \
-printf '%f %s bytes\n' | sort
# At each matching width the AVIF file should be clearly smaller than the WebP;
# if they are near-identical, the Sharp AVIF encoder did not run (check service).
3. Confirm the byte savings and the fallback
# The 1200w AVIF should be roughly 40–50% smaller than the 1200w JPEG.
ls -l dist/_astro/hero.*avif dist/_astro/hero.*jpg
Expected outcomes:
| Check | Expected | Failure meaning |
|---|---|---|
First <source> |
type="image/avif" |
Wrong formats order — WebP would win on AVIF browsers |
| Derivative files | 8 next-gen + 1 jpg | Missing files → a width or format dropped |
| AVIF vs JPEG (1200w) | AVIF ~40–50% smaller | Similar sizes → AVIF encode did not run |
<img> attributes |
width, height, and (if priority) fetchpriority="high" |
Missing dims → CLS risk; missing priority → LCP not prioritised |
Common mistakes and fixes
1. Wrong format order
Anti-pattern: formats={['webp', 'avif']}.
Effect: WebP’s <source> comes first, so every AVIF-capable browser matches WebP and never downloads the smaller AVIF — the compression win is silently discarded.
Fix: always list the smallest format first: formats={['avif', 'webp']}. Source order in the emitted <picture> mirrors the array order.
2. Missing or dishonest sizes
Anti-pattern: providing widths but no sizes, or a sizes="100vw" on an image that is actually half-width.
Effect: without sizes the browser assumes 100vw and over-downloads; with a dishonest sizes it picks a candidate too large or too small for the real box, wasting bytes or blurring.
Fix: always pair widths with a sizes that reflects the layout. Compute it with the method in how to calculate optimal sizes attribute values.
3. Remote image not authorised
Anti-pattern: <Picture src="https://cdn.example.com/hero.jpg" …> with the host missing from config.
Effect: Astro will not fetch and re-encode an arbitrary origin, so the image is emitted at its original bytes — no AVIF, no resizing.
Fix: add the host to image.domains or image.remotePatterns in astro.config.mjs, and pass explicit width/height (Astro cannot infer remote dimensions without inferSize).
4. Confusing densities with widths
Anti-pattern: using densities={[1, 2]} for a fluid hero that spans the viewport.
Effect: densities generates only 1x/2x candidates sized off the base width — fine for a fixed avatar, but for a fluid image it produces too few candidates, so large viewports get an upscaled, soft image.
Fix: use widths={[400, 800, 1200, 1600]} + sizes for fluid images; reserve densities for fixed-size elements. They are mutually exclusive.
5. Applying priority to several images
Anti-pattern: marking the hero, a logo, and the first card all priority.
Effect: each gets fetchpriority="high", flooding the high-priority queue and starving CSS/fonts — First Contentful Paint regresses for everyone.
Fix: reserve priority for the single LCP candidate. Every other <Picture> should omit it and default to lazy loading.
Related
- Astro Image and Picture Components — the full component surface, image service config, and getImage()
- How to configure AVIF fallbacks for Safari 14 — the browser-tier cascade and Safari version boundaries this cascade relies on
- AVIF vs WebP Compression Benchmarks — the file-size data behind ordering AVIF before WebP
- How to calculate optimal sizes attribute values — making the
sizesyou pair withwidthshonest