Skip to content

ADR-030: Image Optimisation Defaults

Accepted

Astro’s built-in <Image /> component uses Sharp for image processing and supports multiple output formats, layout modes, and responsive srcset generation. The starter ships with specific defaults in astro.config.mjs that are non-obvious to users:

image: {
service: {
entrypoint: 'astro/assets/services/sharp',
config: {
limitInputPixels: 268402689, // ~16K x 16K pixels
},
},
responsive: {
globalStyles: true,
layout: 'constrained',
},
domains: [],
remotePatterns: [],
},

These choices affect performance, CLS, and how images behave in layouts. Without documentation, users encounter unexpected behaviour when adding large images or changing layouts.

  • CLS prevention: Images must have explicit dimensions to prevent layout shift
  • Format efficiency: AVIF > WebP > JPEG for compression ratio
  • Responsive by default: Images should serve appropriately sized files to all devices
  • Security: Remote image domains must be explicitly allowlisted
  • Large image support: Some use cases (photography portfolios, high-res artwork) need to process very large source files

Sharp is the only production-ready image processing service for Astro’s static output mode. The alternative (@astrojs/image with Squoosh) is deprecated. Sharp is explicitly configured rather than relying on Astro’s auto-detection to ensure consistent behaviour across environments.

Sharp’s default pixel limit is ~268 megapixels (16,384 × 16,384). This is explicitly set rather than left as a default to:

  1. Document the limit — users adding very large source images (photography, scanned artwork) will hit this limit and need to know it exists
  2. Prevent silent failures — Sharp throws an error rather than silently producing a corrupt output when the limit is exceeded
  3. Allow override — users with legitimate large-image needs can increase this value in astro.config.mjs

For typical web content (photos up to ~6000×4000px from modern cameras), this limit is never reached.

Astro’s responsive image layouts:

LayoutBehaviourUse case
fixedExact pixel dimensions, no scalingIcons, logos
constrainedScales down to fit container, never scales upMost content images
full-widthStretches to fill container widthHero images, banners

constrained is the correct default because:

  • It prevents images from rendering larger than their intrinsic size (no blurry upscaling)
  • It respects container width constraints
  • It generates a srcset with multiple sizes for responsive delivery
  • It sets explicit width and height attributes, preventing CLS

Users who need full-width hero images should pass layout="full-width" explicitly on those components.

This injects a small CSS snippet that applies max-width: 100% and height: auto to all Astro-processed images globally. Without this, images may overflow their containers on narrow viewports.

Why global rather than per-component? The alternative is adding class="w-full h-auto" to every <Image /> usage. Global styles are less error-prone and consistent with how browsers handle <img> elements by default.

Output Formats: single format, AVIF by default

Section titled “Output Formats: single format, AVIF by default”

(Amended 2026-08-13 to ship-truth: the founding text described multi-format <picture> output, which the wrapper does not produce.) The Image wrapper emits a single <img> per source: resolveImageFormat defaults to AVIF (~50% smaller than JPEG), honours an explicit format prop, and passes SVGs through unrasterised. There is no <picture>/multi-format fallback — AVIF support is universal in the template’s supported browsers, and one variant keeps the build and the image cache small.

Remote image optimisation is disabled by default. Users must explicitly allowlist external image domains. This is a security decision — processing arbitrary remote URLs would allow SSRF-style attacks if the site ever processes user-provided URLs.

To enable remote images from a specific domain:

image: {
domains: ['images.unsplash.com'],
// or use remotePatterns for wildcard matching
}

Raw <img> exemptions (amendment 2026-07-05)

Section titled “Raw <img> exemptions (amendment 2026-07-05)”

Constitution rule 8 (“images use the Astro Image component”) is enforced with these exemptions, which the codebase already practised without a record:

  1. The wrapper’s own fallback: src/components/atoms/Image.astro (and the card molecules that follow the same pattern) render AstroImage for ImageMetadata sources and fall back to a raw <img> for plain-string sources (public/ paths, remote URLs outside domains), which the pipeline cannot type or transform.
  2. SVGs that must not be rasterised: the pipeline converts SVG to raster formats, destroying embedded CSS animation and self-painted backgrounds. Files like the Pulci Nella state portraits (ADR-054) and the site logo (public/logo.svg in Header/Footer) use raw <img> deliberately. Each such usage carries an inline comment stating the reason; the Header/Footer logo usages are covered by this amendment.

Anything outside these two cases still halts on rule 8.

  • Images are responsive and CLS-free by default
  • AVIF/WebP output reduces image payload significantly
  • Explicit pixel limit prevents silent failures with large source files
  • Remote image processing is secure by default (opt-in allowlist)
  • constrained layout may surprise users who expect images to fill their container — they must use layout="full-width" explicitly
  • globalStyles: true adds a small CSS injection that users cannot easily override per-image
  • Very large source images (>16K×16K) require increasing limitInputPixels
  • Sharp must be installed as a dependency — it is listed in dependencies (not devDependencies) because Astro’s image service requires it at build time in CI environments
  • Image optimisation only runs during pnpm run build, not in pnpm run dev (dev serves originals for speed)
  • CLS: All images must have explicit width and height — verified via Lighthouse CLS audit
  • Format delivery: Network tab must show AVIF or WebP responses in supporting browsers
  • No layout overflow: Images must not exceed their container width on any viewport
  • Testable consequences:
    • TC-1: no raw <img appears in src/ outside the two recorded exemptions (the wrapper’s string-src fallback; unrasterisable SVGs carrying an inline justifying comment).
    • TC-2: every raster asset in source and build output is within the per-image size budget.
  • Checks:
    • TC-1 → check no-raw-img (status: warn)
    • TC-2 → images:gate in CI (status: block, pre-existing gate) — see ADR-057
  • Not machine-checkable: per-image layout/format choices remain judgment calls.
  • Graduation log: (empty at creation; entries added when a check changes status)

Date: 2026-02-18
Participants: Template maintainers
Outcome: Accepted