Fix Low Resolution Images for Better Best Practices Score

Serve images with appropriate resolution for the display size and device pixel ratio. Blurry images hurt perceived quality.
Harlan WiltonHarlan Wilton3 min read Published

Serving undersized images on high-DPI screens makes your site look amateur. Users notice blurry images even if they can't articulate why something feels "off."

What's Happening

Your image's natural dimensions are too small for how it's being displayed. On a 2x retina display, an image displayed at 400x300 CSS pixels needs to be at least 800x600 actual pixels to look sharp. When you serve a 400x300 image at that size on a retina screen, the browser upscales it and everything looks soft.

Lighthouse calculates the expected size by multiplying the displayed dimensions by the device pixel ratio (DPR). It caps the requirement at 2x because research shows humans can't perceive quality improvements beyond that at typical viewing distances. If your image falls short, it fails.

Diagnose

Chrome DevTools

  1. Right-click the blurry image, select Inspect
  2. In Elements panel, look at the <img> element
  3. Check the rendered size vs natural size:
// In console, click the image element first
$0.getBoundingClientRect() // displayed size
$0.naturalWidth // actual image width
$0.naturalHeight // actual image height

Compare: if displayed width is 400px and DPR is 2, you need naturalWidth of at least 800px.

Quick Page Audit

document.querySelectorAll('img').forEach((img) => {
  const rect = img.getBoundingClientRect()
  const dpr = Math.min(window.devicePixelRatio, 2)
  const expectedWidth = Math.ceil(rect.width * dpr)
  const expectedHeight = Math.ceil(rect.height * dpr)

  if (img.naturalWidth < expectedWidth || img.naturalHeight < expectedHeight) {
    console.warn('Low res:', img.src, {
      displayed: `${rect.width}x${rect.height}`,
      actual: `${img.naturalWidth}x${img.naturalHeight}`,
      expected: `${expectedWidth}x${expectedHeight}`
    })
  }
})

Fix

1. Serve Properly Sized Images

The direct fix: ensure your image files are large enough.

For an image displayed at 600x400 on desktop:

  • 1x displays: 600x400 minimum
  • 2x displays (retina): 1200x800 minimum

If you only serve one size, use the 2x version. It'll look great on retina and acceptable (though heavier) on 1x.

<!-- Single high-res image -->
<img
  src="/hero-1200x800.jpg"
  width="600"
  height="400"
  alt="Hero"
>

2. Use srcset for Multiple Resolutions

Serve different sizes for different pixel densities:

<img
  src="/photo-600.jpg"
  srcset="
    /photo-600.jpg 1x,
    /photo-1200.jpg 2x
  "
  width="600"
  height="400"
  alt="Photo"
>

The browser picks the right one based on DPR.

For responsive widths, use srcset with width descriptors:

<img
  src="/hero-800.jpg"
  srcset="
    /hero-400.jpg 400w,
    /hero-800.jpg 800w,
    /hero-1200.jpg 1200w,
    /hero-1600.jpg 1600w
  "
  sizes="(max-width: 600px) 100vw, 800px"
  width="800"
  height="450"
  alt="Hero"
>

The browser considers both viewport width and DPR when selecting.

3. Generate Multiple Sizes at Build Time

Don't manually create every size. Use tooling:

Sharp (Node.js):

import sharp from 'sharp'

const sizes = [400, 800, 1200, 1600]

sizes.forEach((width) => {
  sharp('original.jpg')
    .resize(width)
    .toFile(`output-${width}.jpg`)
})

Image CDN approach:

<!-- Cloudinary example -->
<img
  src="https://res.cloudinary.com/demo/image/upload/w_800/sample.jpg"
  srcset="
    https://res.cloudinary.com/demo/image/upload/w_400/sample.jpg 400w,
    https://res.cloudinary.com/demo/image/upload/w_800/sample.jpg 800w,
    https://res.cloudinary.com/demo/image/upload/w_1200/sample.jpg 1200w
  "
  sizes="(max-width: 600px) 100vw, 800px"
  alt="Sample"
>

CDNs like Cloudinary, imgix, or Cloudflare Images resize on-the-fly.

4. Consider Modern Formats

While fixing resolution, also consider format. AVIF and WebP compress better, letting you serve larger dimensions without increasing file size:

<picture>
  <source
    srcset="/hero-1200.avif 1200w, /hero-800.avif 800w"
    sizes="(max-width: 600px) 100vw, 800px"
    type="image/avif"
  >
  <source
    srcset="/hero-1200.webp 1200w, /hero-800.webp 800w"
    sizes="(max-width: 600px) 100vw, 800px"
    type="image/webp"
  >
  <img
    src="/hero-800.jpg"
    srcset="/hero-1200.jpg 1200w, /hero-800.jpg 800w"
    sizes="(max-width: 600px) 100vw, 800px"
    width="800"
    height="450"
    alt="Hero"
  >
</picture>

Framework Examples

Nuxt - @nuxt/image generates multiple sizes automatically:
<NuxtImg
  src="/photo.jpg"
  width="800"
  height="600"
  sizes="sm:100vw md:50vw lg:800px"
  format="webp"
/>
The sizes prop triggers srcset generation. Configure providers in nuxt.config.ts for CDN support.
Next.js - next/image handles this automatically:
<Image
  src="/photo.jpg"
  width={800}
  height={600}
  sizes="(max-width: 600px) 100vw, 800px"
  alt="Photo"
/>
It generates multiple sizes and serves WebP/AVIF where supported.

Verify the Fix

  1. Re-run Lighthouse - "Serves images with appropriate resolution" should pass
  2. Visual check - On a retina display, images should look crisp, not soft
  3. DevTools Network - Verify correct srcset variant loads based on your display

Quick console check:

document.querySelectorAll('img').forEach((img) => {
  const rect = img.getBoundingClientRect()
  const dpr = Math.min(window.devicePixelRatio, 2)
  const ratio = img.naturalWidth / (rect.width * dpr)
  if (ratio < 0.75)
    console.warn('Still undersized:', img.src, ratio.toFixed(2))
})

Ratio should be 1.0 or higher (0.75 is the minimum Lighthouse allows for larger images).

Common Mistakes

  • Upscaling small originals - You can't add detail that doesn't exist. Start with high-res source images.
  • Ignoring retina - Testing only on 1x displays misses the problem entirely. Use Chrome DevTools device mode with 2x DPR.
  • CSS background images - This audit only covers <img> elements. Background images need manual srcset via image-set().
  • SVG confusion - SVGs are vector, infinitely scalable. This audit correctly ignores them.

Test Your Entire Site

A product page might have crisp images while your blog uses undersized thumbnails. Unlighthouse audits every page and flags all low-resolution images across your site.