Performance14 min read

11 Responsive Image Mistakes That Ship Broken Layouts

srcset without sizes, lazy-loaded hero images, missing dimensions, art direction in the wrong element. The responsive image errors that quietly waste bandwidth and wreck Core Web Vitals.

By ImageGuide Team·Published August 15, 2026·Updated August 15, 2026
responsive imagessrcsetsizescore web vitalsclslcp

Responsive images have a peculiar failure mode: the page looks completely fine. Nothing is visibly broken, no error appears in the console, and the layout renders correctly on your laptop. Meanwhile a phone downloads a 2400-pixel image to display it at 380 pixels, and the Largest Contentful Paint reading quietly doubles.

These eleven mistakes account for most of it. Each one has a specific fix, and most take a single attribute.

1. Using srcset Without sizes

The most common error by a wide margin.

<!-- Broken: the browser assumes this image is 100% of the viewport -->
<img src="photo-800.jpg"
     srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
     alt="">

Width descriptors (400w) tell the browser how wide each file is. They say nothing about how wide the image will be displayed. Without sizes, the browser falls back to 100vw — it assumes the image fills the viewport.

If your image actually sits in a 400-pixel column on a 1400-pixel-wide screen, the browser picks the largest file for a slot needing the smallest. You have shipped the responsive markup and none of the benefit.

Measured on one image encoded across a normal width ladder:

Bytes Delivered to a 400-Pixel Slot

Same source encoded as WebP q78 at each width. Without sizes, the browser assumes the image fills the viewport.

Five times the bytes for a slot 400 pixels wide, on every page view, for a difference nobody can see. Multiply that by the number of images on a listing page.

<!-- Fixed -->
<img src="photo-800.jpg"
     srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
     sizes="(min-width: 1024px) 400px, (min-width: 640px) 50vw, 100vw"
     alt="" width="800" height="600">

The sizes value is read left to right, and the first matching condition wins. Put the largest breakpoint first.

2. A sizes Value That Lies

Worse than omitting it, because it looks correct in review.

sizes is a promise about your layout, and the browser believes it completely. It selects a file before any CSS is applied, so it cannot check. If your CSS says the image occupies 33% of a container that is itself 80% of the viewport, and your sizes says 50vw, every user gets the wrong file forever.

Verify it rather than reasoning about it:

  1. Open DevTools, select the image, and read its rendered width in the box model
  2. Compare against what sizes claims at that viewport width
  3. Repeat at each breakpoint

Chrome DevTools also shows “Intrinsic size” versus “Rendered size” when you hover an image in the Elements panel. If intrinsic is more than about 1.5× rendered on a 1× display, sizes is wrong. Our DevTools debugging guide covers the workflow.

When your layout is complex enough that sizes is hard to compute, that is a signal to use a simpler value that is honest rather than a precise one that is wrong.

3. Lazy Loading the Hero Image

<!-- Broken: this is the LCP element -->
<img src="hero.jpg" loading="lazy" alt="">

loading="lazy" defers the request until the browser has done layout and decided the image is near the viewport. For an image already in the viewport, that adds a delay to the very element the Largest Contentful Paint measures.

The rule is simple: lazy-load everything below the fold, and nothing above it.

<!-- The hero -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="" width="1600" height="900">

<!-- Everything further down -->
<img src="later.jpg" loading="lazy" decoding="async" alt="" width="800" height="600">

This one is easy to introduce accidentally, because applying loading="lazy" globally through a CMS or a component default feels like an optimisation. It is, for most images, and a regression for the one that matters most. Our lazy loading guide covers the boundary.

4. Missing width and height

<!-- Broken: the browser cannot reserve space -->
<img src="photo.jpg" alt="">

Without dimensions, the browser does not know how tall the image will be until it arrives. It lays out the page as if the image were zero pixels tall, then reflows everything below when it loads. That is Cumulative Layout Shift, and it is the most common self-inflicted Core Web Vitals failure.

Always set both attributes to the intrinsic pixel dimensions of the file:

<img src="photo.jpg" alt="" width="1600" height="900">

They are not display dimensions. Modern browsers use the ratio between them to reserve space, then your CSS scales it:

img {
  max-width: 100%;
  height: auto;
}

That height: auto is required. Without it, the height attribute wins and the image distorts as it scales.

5. Using srcset for Art Direction

srcset and <picture> solve different problems, and mixing them up produces images that are technically correct and visually wrong.

srcset is for resolution switching. Same image, different sizes. The browser chooses, and its choice is a hint you cannot rely on — it may consider network conditions or a user preference.

<picture> with media is for art direction. Different crops or different images entirely. Here the choice is yours, and it is respected exactly.

<!-- Art direction: a tight crop on phones, wide on desktop -->
<picture>
  <source media="(min-width: 1024px)" srcset="hero-wide.jpg">
  <source media="(min-width: 640px)" srcset="hero-square.jpg">
  <img src="hero-portrait.jpg" alt="" width="800" height="1000">
</picture>

If you need a guarantee about which file loads, use <picture>. If you are happy for the browser to optimise, use srcset.

6. Forgetting the img Inside picture

<!-- Broken: nothing renders in some cases, and there is no alt text -->
<picture>
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
</picture>

<picture> is not an element that displays anything. It is a wrapper that tells the contained <img> which file to use. The <img> is mandatory — it carries the alt, the width, the height, and the fallback source.

<picture>
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="Description" width="1600" height="900">
</picture>

Order matters too: browsers take the first <source> they can decode, so put the most efficient format first. AVIF, then WebP, then the <img> fallback.

7. Ignoring Device Pixel Ratio, or Overcorrecting

Two opposite mistakes, both common.

Serving one size to everyone means a 3× phone display renders your 800-pixel image across 800 CSS pixels of screen containing 2400 device pixels. It looks soft.

Serving 3× to everyone means a desktop user on a 1× monitor downloads nine times the pixels they can display.

Width descriptors with an honest sizes handle this automatically — the browser multiplies by its own device pixel ratio when choosing. That is the main reason to prefer w descriptors over x descriptors.

Use x descriptors only for fixed-size images such as logos and icons:

<img src="logo.png" srcset="logo.png 1x, logo@2x.png 2x" alt="" width="200" height="60">

Note also that returns diminish sharply. The visible difference between 2× and 3× on a phone is marginal, while the file is 2.25 times larger. Capping at 2× is a defensible decision. See our retina and HiDPI guide.

8. Too Many or Too Few srcset Entries

Too few — typically two — means most viewports get a file much larger than they need, because the browser rounds up to the next available size.

Too many — a file every 100 pixels — bloats your build, your cache and your storage for savings nobody perceives.

A sensible ladder roughly doubles the pixel count each step:

srcset="photo-400.jpg 400w,
        photo-800.jpg 800w,
        photo-1200.jpg 1200w,
        photo-1600.jpg 1600w,
        photo-2400.jpg 2400w"

Four to six entries covers essentially every device. Choose the widths from your actual layout breakpoints rather than from round numbers, and stop at the largest size your layout can ever display, multiplied by two.

9. Upscaling in the Generator

A subtle one that appears when the ladder is generated automatically.

If your source is 900 pixels wide and your build produces 400, 800, 1200, 1600 and 2400 variants, the last three are upscaled. They are blurry, and they are larger files than the original because interpolation adds no detail but does add bytes.

Every resizing tool has a switch for this. Use it:

// sharp
sharp(input).resize({ width: 1600, withoutEnlargement: true })
# ImageMagick: the > only shrinks, never enlarges
magick photo.jpg -resize 1600x1600\> photo-1600.jpg

Then omit the entries your source cannot fill.

10. Background Images With No Strategy

CSS background images are invisible to srcset entirely, and they are frequently the largest asset on a page.

Use image-set(), which is the CSS equivalent and is now widely supported:

.hero {
  background-image: image-set(
    url("hero.avif") type("image/avif"),
    url("hero.webp") type("image/webp"),
    url("hero.jpg") type("image/jpeg")
  );
}

Combine it with media queries for different sizes:

.hero { background-image: image-set(url("hero-800.webp") type("image/webp")); }

@media (min-width: 1024px) {
  .hero { background-image: image-set(url("hero-1600.webp") type("image/webp")); }
}

Two further points. A CSS background is discovered late, because the browser must download and parse the stylesheet before it knows the image exists — so a hero background starts later than an <img> would. And a background image cannot have alt text, so anything conveying information should be an <img> instead.

If a background image is your LCP element, preload it:

<link rel="preload" as="image" href="hero.webp" fetchpriority="high">

See our preloading and priority hints guide.

11. object-fit Without an Aspect Ratio

/* The image fills the box, but the box has no height until the image loads */
.card img {
  width: 100%;
  object-fit: cover;
}

object-fit: cover controls how the image fills its box. It does not give the box a size. In a card grid, that means every card collapses until its image arrives, then jumps to full height — layout shift again, in the place users are most likely to be scrolling.

.card img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

aspect-ratio reserves the space immediately. Add object-position when the subject is not centred:

.card img {
  aspect-ratio: 3 / 2;
  object-fit: cover;
  object-position: center 30%;
}

Cropping a portrait to a landscape card with the default centre position cuts heads off. This is worth checking on real content rather than on placeholder images.

A Correct Example

Everything above, applied:

<!-- Hero: eager, prioritised, art-directed -->
<picture>
  <source media="(min-width: 1024px)"
          srcset="hero-wide-1600.avif 1600w, hero-wide-2400.avif 2400w"
          sizes="100vw" type="image/avif">
  <source media="(min-width: 1024px)"
          srcset="hero-wide-1600.webp 1600w, hero-wide-2400.webp 2400w"
          sizes="100vw" type="image/webp">
  <img src="hero-portrait-800.jpg"
       srcset="hero-portrait-400.jpg 400w, hero-portrait-800.jpg 800w"
       sizes="100vw"
       alt="Description of the image"
       width="800" height="1000"
       loading="eager" fetchpriority="high" decoding="async">
</picture>

<!-- Below the fold: lazy, in a known-size box -->
<img src="card-800.jpg"
     srcset="card-400.jpg 400w, card-800.jpg 800w, card-1200.jpg 1200w"
     sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
     alt="Description"
     width="800" height="533"
     loading="lazy" decoding="async"
     style="aspect-ratio: 3 / 2; object-fit: cover;">

Or Skip Most of It

Almost every mistake here comes from generating and maintaining the ladder by hand.

A framework image component — next/image, Astro’s <Picture>, Nuxt Image — generates the widths, writes srcset, sets the dimensions and applies lazy loading correctly by default. You still have to get sizes right, because only you know the layout.

An image CDN removes the build step as well, deriving every size and format from URL parameters against one master file. Sirv handles format negotiation and resizing that way, with a free tier to test on your own images.

Summary

The Eleven Mistakes

# Mistake Fix
1 srcset without sizes Add an honest sizes
2 A sizes value that lies Verify against rendered width in DevTools
3 Lazy-loading the hero loading="eager" fetchpriority="high"
4 Missing width and height Set intrinsic dimensions, plus height: auto
5 srcset used for art direction Use <picture> with media
6 <picture> with no <img> The <img> is mandatory
7 Ignoring or overdoing DPR Use w descriptors, cap at 2×
8 Too many or too few entries Four to six, doubling each step
9 Upscaling in the generator withoutEnlargement, or > in ImageMagick
10 Unmanaged background images image-set(), and preload the LCP one
11 object-fit without aspect-ratio Reserve the box before the image lands

Checklist

  1. ✅ Every srcset with w descriptors has a matching sizes
  2. ✅ Rendered width was checked against sizes at each breakpoint
  3. ✅ The LCP image is eager and prioritised
  4. ✅ Every <img> has width and height
  5. ✅ Art direction uses <picture media>, not srcset
  6. ✅ No generated variant is larger than its source
  7. ✅ Boxes have an aspect-ratio wherever object-fit is used

Fix 1, 3 and 4 first. Between them they account for most of the responsive-image damage on a typical site, and all three are one-attribute changes.

Related Resources

Format References

Ready to optimize your images?

Sirv automatically optimizes, resizes, and converts your images. Try it free.

Start Free Trial