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.

Image sources generated before and after adding densities Two panels. Before, the component generated one 720 pixel WebP file and no srcset, so a 2x display upscaled it to 1,440 device pixels. After, it generated 720, 1,080 and 1,440 pixel files with a density srcset, so a 2x display picks the 1,440 pixel file and a 1x display still downloads the 720 pixel file. What the browser can choose from Before one 720 px file no srcset 2x screen: upscaled ×2 soft on 64% of desktops After 720 / 1080 / 1440 px density srcset 2x screen: 1440 px file sharp at every density
1x readers download the same bytes as before; only high-density screens fetch the larger files.

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:

SettingMedian size (2× variant)Text sharpness (blind rating, 1–5)
WebP q7561 KB3.1
WebP q9098 KB4.3
WebP lossless142 KB4.9
AVIF q7054 KB4.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.

File size and text sharpness rating for screenshot encoding settings Paired bars for four encoding settings. WebP quality 75, 61 kilobytes and sharpness 3.1 out of 5. WebP quality 90, 98 kilobytes and 4.3. WebP lossless, 142 kilobytes and 4.9. AVIF quality 70, 54 kilobytes and 4.4. Size and sharpness by setting Size (KB) Sharpness ×10 WebP q75 61 31 WebP q90 98 43 WebP lossless 142 49 AVIF q70 54 44
Median of 20 screenshots at the 2x width; sharpness rated blind by four people on a 2x display, scaled ×10 on the chart.

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.

How the undersized image check works Four steps. Load sampled pages in Playwright with a device scale factor of 2. For each image in main, read naturalWidth, clientWidth and devicePixelRatio. Flag any image whose natural width is below 90 percent of client width times device pixel ratio. Fail the pull request with the image URL and both widths. Catching soft images before release Load at DPR 2 40 sampled pages Measure each img natural vs client × DPR Below 90%? flag it Fail the PR URL + widths
Lighthouse only flags oversized images; this check covers the opposite failure.

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

MeasureBeforeAfterTool
Images undersized on 2× (40 pages)3120Playwright check
Median screenshot bytes, 1× readers38 KB36 KB (AVIF)DevTools
Median screenshot bytes, 2× readers38 KB (upscaled)54 KBDevTools
Blurry-image reports / month60Docs feedback widget
LCP p75, docs, mobile1.7 s1.7 sweb-vitals RUM
Build time (cold)2 min 40 s3 min 30 sCI 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 width with no densities or widths. High-density screens upscale it.
  • Copied sizes values. 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.

Filed underFixesAstro