ADR-030: Image Optimisation Defaults
Status
Section titled “Status”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.)
Context
Section titled “Context”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.
Decision Drivers
Section titled “Decision Drivers”- 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
Decisions
Section titled “Decisions”Sharp as the Image Service
Section titled “Sharp as the Image Service”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.
limitInputPixels: 268402689 (~16K × 16K)
Section titled “limitInputPixels: 268402689 (~16K × 16K)”Sharp’s default pixel limit is ~268 megapixels (16,384 × 16,384). This is explicitly set rather than left as a default to:
- Document the limit — users adding very large source images (photography, scanned artwork) will hit this limit and need to know it exists
- Prevent silent failures — Sharp throws an error rather than silently producing a corrupt output when the limit is exceeded
- 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.
layout: 'constrained'
Section titled “layout: 'constrained'”(Amended 2026-09-12: the key is image.layout; it was recorded under a
non-existent image.responsive object.) Astro’s responsive image layouts:
| Layout | Behaviour | Use case |
|---|---|---|
fixed | Exact pixel dimensions, no scaling | Icons, logos |
constrained | Scales down to fit container, never scales up | Most content images |
full-width | Stretches to fill container width | Hero 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
srcsetwith multiple sizes for responsive delivery - It sets explicit
widthandheightattributes, 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.)
responsiveStyles: true
Section titled “responsiveStyles: true”(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.
domains: [] and remotePatterns: []
Section titled “domains: [] and remotePatterns: []”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:
- The wrapper’s own fallback:
src/components/atoms/Image.astro(and the card molecules that follow the same pattern) renderAstroImageforImageMetadatasources and fall back to a raw<img>for plain-string sources (public/ paths, remote URLs outsidedomains), which the pipeline cannot type or transform. - 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.svgin 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.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- 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)
Negative
Section titled “Negative”constrainedlayout may surprise users who expect images to fill their container — they must uselayout="full-width"explicitlyglobalStyles: trueadds a small CSS injection that users cannot easily override per-image- Very large source images (>16K×16K) require increasing
limitInputPixels
Neutral
Section titled “Neutral”- Sharp must be installed as a dependency — it is listed in
dependencies(notdevDependencies) because Astro’s image service requires it at build time in CI environments - Image optimisation only runs during
pnpm run build, not inpnpm run dev(dev serves originals for speed)
Validation
Section titled “Validation”- CLS: All images must have explicit
widthandheight— 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
References
Section titled “References”- Astro Image Documentation
- Sharp Documentation
- Core Web Vitals: CLS
- ADR-020: Page Performance Patterns
- Performance Budgets
Enforcement
Section titled “Enforcement”- Testable consequences:
- TC-1: no raw
<imgappears insrc/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-descriptorsrcsetand a derivedsizes— and an SVG does not. (Added 2026-09-12.) - TC-4:
Picturefromastro:assetsis imported only bysrc/pages/showcase.astro, and the demo renders AVIF and WebP sources over a JPEG fallback. (Added 2026-10-20.)
- TC-1: no raw
- Checks:
- TC-1 → check
no-raw-img(status: warn) - TC-2 →
images:gatein CI (status: block, pre-existing gate) — see ADR-057 - TC-3 →
src/components/atoms/__tests__/Image.test.tsviatest:unit(status: block) - TC-4 →
src/__tests__/picture-scope.test.tsviatest:unit(status: block); the rendered sources →e2e/showcase.spec.tsin CI
- TC-1 → check
- Not machine-checkable: per-image layout/format choices remain judgment calls.
- Graduation log:
- 2026-10-12 —
no-raw-imgstays warn; miscalibration fixed and backlog cleared (11 findings to 0, #435). The check matched an<imgnamed in a code comment and in a string in a page’s frontmatter; it now reads markup only (no.astrofrontmatter, no comments) while still taking theADR-030justification from the comment above the tag. The other nine were legitimate exemptions missing the keyword. Calibration window restarts from this date.
- 2026-10-12 —
Date: 2026-02-18
Participants: Template maintainers
Outcome: Accepted