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."
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.
<img> element// 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.
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}`
})
}
})
The direct fix: ensure your image files are large enough.
For an image displayed at 600x400 on desktop:
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"
>
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.
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.
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>
@nuxt/image generates multiple sizes automatically:<NuxtImg
src="/photo.jpg"
width="800"
height="600"
sizes="sm:100vw md:50vw lg:800px"
format="webp"
/>
sizes prop triggers srcset generation. Configure providers in nuxt.config.ts for CDN support.next/image handles this automatically:<Image
src="/photo.jpg"
width={800}
height={600}
sizes="(max-width: 600px) 100vw, 800px"
alt="Photo"
/>
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).
<img> elements. Background images need manual srcset via image-set().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.
Image Aspect Ratio
Display images with their natural aspect ratio to prevent distortion. Stretched or squished images look unprofessional.
Inspector Issues
Resolve browser-detected problems including mixed content, cookie issues, CSP violations, and cross-origin blocks that appear in Chrome's Issues panel.