The problem
Astro's built-in <Image /> only outputs a single format. To get AVIF (best) → WebP (fallback) → JPG (last resort), you have to wrap a <picture> element by hand - doing that on every page becomes repetitive boilerplate.
The snippet below packages that pattern into a single reusable component.
File: src/components/SmartImage.astro
---
import { getImage } from 'astro:assets';
import type { ImageMetadata } from 'astro';
interface Props {
src: ImageMetadata;
alt: string;
widths?: number[];
sizes?: string;
loading?: 'lazy' | 'eager';
class?: string;
}
const {
src,
alt,
widths = [400, 800, 1200, 1600],
sizes = '(min-width: 1024px) 1200px, 100vw',
loading = 'lazy',
class: className,
} = Astro.props;
async function setOf(format: 'avif' | 'webp' | 'jpg') {
const imgs = await Promise.all(
widths.map((w) => getImage({ src, format, width: w })),
);
return imgs.map((i, idx) => `${i.src} ${widths[idx]}w`).join(', ');
}
const avif = await setOf('avif');
const webp = await setOf('webp');
const jpg = await setOf('jpg');
const fallback = await getImage({ src, format: 'jpg', width: widths[0] });
---
<picture class={className}>
<source type="image/avif" srcset={avif} sizes={sizes} />
<source type="image/webp" srcset={webp} sizes={sizes} />
<source type="image/jpeg" srcset={jpg} sizes={sizes} />
<img
src={fallback.src}
alt={alt}
loading={loading}
decoding="async"
width={fallback.attributes.width}
height={fallback.attributes.height}
/>
</picture>
Usage
---
import SmartImage from '../components/SmartImage.astro';
import hero from '../assets/hero.jpg';
---
<SmartImage
src={hero}
alt="Hero banner"
widths={[640, 1024, 1600, 2400]}
sizes="(min-width: 1280px) 1280px, 100vw"
loading="eager"
/>
Measured results on natecue.com
- LCP image: ~140 KB (JPG) → ~38 KB (AVIF) - ~73% reduction.
- CLS = 0 because
width/heightare auto-filled by Astro from ImageMetadata. - Lighthouse Performance score: 92 → 98.
Notes
- AVIF encoding is significantly slower than WebP - only run
astro buildat deploy time, not during development. - For SVG/GIF: bypass this component and use a plain
<img>tag directly. - When deploying on Vercel/Netlify: make sure to enable long-term caching for
_astro/*(immutable, 1 year).