Fixing Blurry Images From astro:assets Resizing
Astro's <Image> and <Picture> components make it easy to ship small, modern images: give them a source and a width, and they produce WebP or AVIF at the right size. Then someone opens the site on a laptop with a high-density display, and the product screenshots look soft — the text in them slightly smeared, the UI edges fuzzy. Nothing is broken; the components did exactly what they were told. They were told to make an image 800 pixels wide, and a 2× display needs 1,600.
This guide fixes soft images on an Astro 5 documentation site with about 900 product screenshots, where readers on high-density displays (64% of desktop sessions) reported blurry text in images. It covers widths and densities, the sizes attribute, screenshot-specific quality settings, and a check that catches undersized images. It belongs to Image Optimization Pipelines in Astro.
Symptoms
- Screenshots in docs looked sharp on 1× monitors and soft on 2× and 3× screens.
- DevTools showed each
<img>rendering at 720 CSS pixels with a natural width of 720 — on a 2× display, 1,440 device pixels stretched from 720. - Lighthouse reported no problem: its "properly size images" audit flags images that are too large, not too small.
Cause 1: One Width, No Densities
The docs layout rendered Markdown images through a custom component:
<Image src={src} alt={alt} width={720} format="webp" />
width={720} produced exactly one file, 720 pixels wide, with no srcset. The browser had nothing better to choose. Adding densities produces 1×, 1.5× and 2× variants with a density-descriptor srcset:
<Image src={src} alt={alt} width={720} densities={[1, 1.5, 2]} format="webp" quality={82} />
For images whose displayed width varies with the viewport, widths with a sizes attribute is the better tool (next section). For a fixed-width docs column, densities are simpler and correct.
Cause 2: A sizes Attribute That Lied
Hero and feature images on marketing pages used widths but had copied sizes="(max-width: 768px) 100vw, 50vw" from an example. The actual layout put the image in a 1,120-pixel column at desktop widths — closer to 80vw on a 1,440-pixel window. The browser, told the image was 50vw (720 CSS pixels), picked the 1,440 file for a 2× display when 2,240 device pixels were needed. Measuring the rendered width at each breakpoint and writing sizes to match:
<Picture
src={hero} alt={alt}
widths={[640, 960, 1280, 1680, 2240]}
sizes="(max-width: 48rem) calc(100vw - 2rem), (max-width: 80rem) calc(100vw - 8rem), 1120px"
formats={['avif', 'webp']}
/>
The calc() values account for page padding. A helpful check is DevTools' "Rendered size" and "Intrinsic size" on hover in the Elements panel at several window widths: intrinsic should be at least rendered × device pixel ratio.
Cause 3: Fractional Downscaling of Screenshots
Product screenshots were captured at 1,920 pixels wide on 2× displays — 3,840 device pixels of source. Downscaling to 1,440 is a factor of 0.375, and text rendered at that scale became noticeably softer than a clean 0.5 (1,920 → 960 pairs nicely) because resampling filters smear thin strokes at awkward ratios. For screenshots, the pipeline now captures at a width that halves cleanly to the display sizes: screenshots are taken at 1,440 CSS pixels on a 2× display (2,880 device pixels), and the variants are 1,440 (exactly half) and 2,880 (original). Text stayed crisp at both.
Cause 4: Lossy Compression on Text
Even at the right size, screenshots of code and small UI text showed ringing around letters at WebP quality 75, the default. Screenshots are a different kind of image from photographs: large flat areas and sharp edges. Three options were compared on 20 representative screenshots:
| Setting | Median size (2× variant) | Text sharpness (blind rating, 1–5) |
|---|---|---|
| WebP q75 | 61 KB | 3.1 |
| WebP q90 | 98 KB | 4.3 |
| WebP lossless | 142 KB | 4.9 |
| AVIF q70 | 54 KB | 4.4 |
AVIF at quality 70 matched WebP 90 for sharpness at about half the size, so screenshots use AVIF first with WebP q90 as fallback through <Picture>. Photographs stay at the default quality.
Step 2: Separate Screenshots From Photos
Different content needs different settings, so the Markdown image component picks a preset from the file's folder or a front matter hint:
const PRESETS = {
screenshot: { formats: ['avif', 'webp'], quality: { avif: 70, webp: 90 }, densities: [1, 2] },
photo: { formats: ['avif', 'webp'], quality: { avif: 55, webp: 78 }, widths: [480, 960, 1440, 1920] },
};
const preset = src.src.includes('/screenshots/') ? PRESETS.screenshot : PRESETS.photo;
Step 3: Check for Undersized Images
Because Lighthouse does not flag undersized images, a Playwright check does: for each <img> on sampled pages at a 2× device scale factor, it compares naturalWidth against clientWidth × devicePixelRatio and fails below 0.9:
const under = await page.$$eval('main img', (imgs) => imgs
.filter((i) => i.complete && i.naturalWidth < i.clientWidth * devicePixelRatio * 0.9)
.map((i) => `${i.currentSrc} ${i.naturalWidth} < ${i.clientWidth}×${devicePixelRatio}`));
expect(under).toEqual([]);
It runs on 40 pages at 1× and 2× in about 30 seconds.
Balancing Sharpness and Bytes on Mobile
Phones are where extra pixels cost the most and are noticed the least: a 3× phone displaying a 360-CSS-pixel screenshot would want a 1,080-pixel file. The site caps density variants at 2× — the densities list stops at 2 — because blind comparisons on a 3× phone showed no visible difference between 2× and 3× files for screenshots at that size, while the 3× files were 70% larger. For photos with widths and sizes, the browser's own choice usually lands near 2× on high-density phones already.
Measured Impact
| Measure | Before | After | Tool |
|---|---|---|---|
| Images undersized on 2× (40 pages) | 312 | 0 | Playwright check |
| Median screenshot bytes, 1× readers | 38 KB | 36 KB (AVIF) | DevTools |
| Median screenshot bytes, 2× readers | 38 KB (upscaled) | 54 KB | DevTools |
| Blurry-image reports / month | 6 | 0 | Docs feedback widget |
| LCP p75, docs, mobile | 1.7 s | 1.7 s | web-vitals RUM |
| Build time (cold) | 2 min 40 s | 3 min 30 s | CI logs |
High-density readers now download about 40% more image bytes — the cost of being sharp — while 1× readers download slightly less thanks to AVIF. LCP did not change because docs pages' LCP element is text. The longer cold build is recovered with the image cache, as in Caching Processed Images Between Builds.
Pitfalls & Rollback
- A single
widthwith no densities or widths. High-density screens upscale it. - Copied
sizesvalues. Measure the rendered width at each breakpoint. - Awkward downscale ratios for screenshots. Capture at sizes that halve cleanly.
- Photo quality settings for screenshots. Text needs higher quality or AVIF.
- Rollback: presets are in one component; reverting it restores single-width output, and the Playwright check will report every undersized image.
Conclusion
Soft images from astro:assets are almost always too few pixels, not too much compression: a single width with no densities, a sizes attribute smaller than the real layout, screenshots downscaled at awkward ratios, and photo-grade quality on text. Adding densities, measuring sizes, capturing screenshots at clean scales and giving them their own AVIF preset made every image sharp on 2× displays, and a Playwright check catches undersized images that Lighthouse never reports.
FAQ
Why are my Astro images blurry on a retina screen?
Usually because only a 1x image was generated. If the Image component is given a width of 800, it produces one 800-pixel file, which a 2x display stretches to 1,600 device pixels. Generating several widths with the widths or densities props and a correct sizes attribute lets the browser pick a file sharp enough for the screen.
What should the sizes attribute be?
The width the image will occupy in CSS pixels at each breakpoint, written as media conditions, for example max-width 48rem then 100vw, otherwise 720 pixels. The browser multiplies that by the device pixel ratio to choose a source. A sizes value that is too small makes the browser pick a file that is too small, which is the most common cause of soft images.
Does a higher quality setting fix blurry images?
Rarely. Blur from too few pixels looks soft at any quality. Quality settings affect compression artefacts such as blocking and banding, which look different from blur. Fix the dimensions first; only raise quality if you see artefacts at the correct size, and check the file-size cost.
Are screenshots of text a special case?
Yes. Screenshots with small text and sharp edges suffer more from both downscaling and lossy compression. Serve them at enough width for 2x displays, prefer lossless WebP or PNG for small screenshots, or use AVIF and WebP at higher quality, and avoid resizing screenshots to fractional scale factors that blur text.
Related
- Parent: Image Optimization Pipelines in Astro — the full image pipeline.
- Art Direction with the Picture Element — different crops per breakpoint.
- Serving AVIF With Fallbacks on Static Sites — the AVIF setup used for screenshots.
- Serving Astro Images Through Cloudflare Image Resizing — resizing at the edge instead of the build.
- Managing Images in Docs-as-Code Repos — capturing and storing screenshots.