Skip to content

ADR-030: Image Optimisation Defaults

Accepted (amended 2026-09-12: the config block below was recorded as image.responsive: { globalStyles, layout }, a key Astro has never had — zod stripped it silently, the build stayed green, and no responsive default applied from 2025-09-29 until the 2026-09 showcase audit caught it. The shipped keys are image.layout and image.responsiveStyles; the excerpt, the section headings and the wrapper description are corrected inline, and the behaviour is now pinned by a Container-API microtest — see Enforcement. The decisions themselves are unchanged.)

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
},
},
layout: 'constrained',
responsiveStyles: true,
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.

(Amended 2026-09-12: the key is image.layout; it was recorded under a non-existent image.responsive object.) 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.

With the layout active, Astro derives widths and sizes from the rendered width when the caller gives none, and ignores densities (its Props type also forbids densities next to layout). The wrapper (src/components/atoms/Image.astro) therefore no longer invents default widths or densities of its own and no longer accepts a densities prop: a caller’s explicit widths or sizes override the derivation, SVG sources are rendered with layout="none" (a vector has no widths to generate), and the layout prop is passed through so a single image can opt into full-width or out with none. (Amended 2026-09-12; before this the wrapper’s defaults were the only reason any raster carried a srcset.)

(Amended 2026-09-12: the key is image.responsiveStyles; it was recorded as globalStyles.) This injects a small CSS snippet that applies max-width: 100% to constrained images and width: 100% to full-width ones, plus the object-fit / object-position wiring for the fit and position props. 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.

Amendment 2026-10-20 (#493): <Picture> as a showcase-only example. The single-format decision above stands for every image the site renders. One exception is recorded: src/pages/showcase.astro may render Astro’s <Picture> (from astro:assets) to show what multi-format output looks like and why the starter does not use it by default. The demo emits AVIF and WebP <source> elements over a JPEG fallback <img>, from the same JPEG source the Image demo uses. The showcase’s job is to explain the starter’s choices, and a choice is easier to judge when the alternative is visible next to it. The cost is a few extra variants of one image in the build, which are counted by the ADR-057 image budget like any other output. No other file may import Picture. Multi-format output elsewhere still needs its own amendment. TC-4 below pins this.

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.

Amendment 2026-10-12 (#435): the MDX Figure component is recorded under exemption 1. Its src prop is a string by contract (a remote URL or a public/ path written in MDX), the same case as the wrapper’s fallback, and it now sets loading="lazy" and decoding="async" as the wrapper does. Every exempt <img> in src/ carries an inline comment naming ADR-030 and which exemption applies.

Amendment 2026-09-23 (#414): the homepage’s three state portraits (state-shipped, state-writing, state-thinking) no longer use this exemption. They are imported as Astro SVG components and inlined, which keeps their CSS animation without a raw <img>. Because inlined markup shares the page’s id and class namespace, those files prefix every id and class with pn-, guarded by src/__tests__/homepage-inline-svg.test.ts. Exemption 2 itself stands: the site logo and other unrasterisable SVGs rendered as files still need it, and Astro’s SVG component is the preferred path where inlining is safe.

  • 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.
    • TC-3: a raster rendered through the wrapper carries the constrained layout — data-astro-image, a width-descriptor srcset and a derived sizes — and an SVG does not. (Added 2026-09-12.)
    • TC-4: Picture from astro:assets is imported only by src/pages/showcase.astro, and the demo renders AVIF and WebP sources over a JPEG fallback. (Added 2026-10-20.)
  • Checks:
    • TC-1 → check no-raw-img (status: warn)
    • TC-2 → images:gate in CI (status: block, pre-existing gate) — see ADR-057
    • TC-3 → src/components/atoms/__tests__/Image.test.ts via test:unit (status: block)
    • TC-4 → src/__tests__/picture-scope.test.ts via test:unit (status: block); the rendered sources → e2e/showcase.spec.ts in CI
  • Not machine-checkable: per-image layout/format choices remain judgment calls.
  • Graduation log:
    • 2026-10-12 — no-raw-img stays warn; miscalibration fixed and backlog cleared (11 findings to 0, #435). The check matched an <img named in a code comment and in a string in a page’s frontmatter; it now reads markup only (no .astro frontmatter, no comments) while still taking the ADR-030 justification from the comment above the tag. The other nine were legitimate exemptions missing the keyword. Calibration window restarts from this date.

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