Fixing Responsive Images That Serve the Wrong Size and Waste Bandwidth on Mobile
A page has an <img> with a srcset and looks correctly responsive in DevTools, yet a Lighthouse audit or a real mobile user's network log shows the browser downloading the full 2000px desktop image for a 360px-wide phone screen. The markup isn't broken; the browser's own size-selection logic is working exactly as instructed — the instructions themselves are wrong.
The Problem
An image is marked up with srcset and sizes, or served through a framework's image component, and it visually looks responsive — it scales to fit its container at every viewport width. But a network trace on an actual phone, or Lighthouse's "Properly size images" audit, shows the browser is downloading a much larger image file than the space it's rendered into actually needs — a 2000px-wide source for a 360px-wide display slot, wasting bandwidth and directly hurting Largest Contentful Paint on mobile connections. The image tag technically has a srcset; the browser is choosing correctly from what it was given — the candidate list or the sizing hint just doesn't reflect the image's real rendered dimensions.
Why It Happens
A missing or inaccurate sizes attribute makes the browser guess based on the viewport, not the actual rendered width
Without an explicit sizes attribute, the browser defaults to assuming the image will render at 100% of the viewport width — which is wrong for any image inside a constrained container (a card, a sidebar, a grid column), and causes the browser to select a candidate sized for the full viewport instead of the image's actual, smaller display size.
The srcset candidate list is often copied once and never actually matches the container's real breakpoints
A srcset with width descriptors (image-400.jpg 400w, image-800.jpg 800w, image-1600.jpg 1600w) only helps if those widths correspond to how large the image is actually displayed at different viewports. A generic set of breakpoints copied from another part of the site, without checking the actual rendered width in this specific layout, can leave every available candidate too large for the real use case.
CMS or upload pipelines frequently only generate a single, large master image
If the only image variant that exists is the original upload — often sized for print or for a hero banner elsewhere on the site — there's no smaller candidate for srcset to actually offer, regardless of how correctly the markup itself is written. The browser can only choose among the sizes it's given.
Framework image components can silently fall back to unoptimized behavior when misconfigured
Image components in modern frameworks (Next.js's Image, Gatsby's GatsbyImage, and similar) generate the correct srcset automatically — but only when given accurate width/height or fill-container information. A misconfigured or missing layout hint can cause the component to generate a candidate set based on an assumed size that doesn't match the actual rendered container.
The Fix
1. Set an accurate sizes attribute that matches the image's real rendered width at each breakpoint
<img
srcset="card-400.jpg 400w, card-800.jpg 800w, card-1200.jpg 1200w"
sizes="(max-width: 600px) 90vw, (max-width: 1024px) 45vw, 30vw"
src="card-800.jpg"
alt="Product photo"
/>
The sizes attribute has to describe the image's actual rendered width in the real layout — not the viewport width — at each relevant breakpoint. This is what lets the browser correctly calculate which srcset candidate is genuinely large enough, instead of defaulting to the full-viewport assumption.
2. Generate srcset candidates that actually bracket the image's real display sizes
# Generate purpose-built variants instead of reusing one generic set
npx sharp-cli resize 400 -- input.jpg -o card-400.jpg
npx sharp-cli resize 800 -- input.jpg -o card-800.jpg
npx sharp-cli resize 1200 -- input.jpg -o card-1200.jpg
Generating candidates sized specifically around the container's actual rendered widths — checked directly in the browser's DevTools element inspector, not assumed — ensures every option in the srcset list is a genuinely useful choice rather than all being oversized for the container in question.
3. Use a framework's image component with accurate layout information instead of a raw img tag where available
import Image from "next/image";
<Image
src="/product.jpg"
width={400}
height={300}
sizes="(max-width: 600px) 90vw, 30vw"
alt="Product photo"
/>
A properly configured framework image component generates the correct srcset and sizes automatically from accurate dimension props, removing the manual bookkeeping error that causes a hand-written srcset to drift out of sync with the actual layout over time.
4. Fix the upload pipeline to generate multiple sizes, not just serve the original
# CMS/upload-time processing, not just runtime serving
npx sharp-cli resize 400,800,1200,1600 -- uploaded-image.jpg -o dist/
If the underlying asset pipeline only stores one master image, no amount of correct markup can produce a genuinely smaller candidate. Generating a proper size ladder at upload or build time — rather than only at request time through a CDN transform, if that's not already in place — ensures smaller variants actually exist to be selected.
Why This Works
Each fix addresses a different point where the browser's size-selection input can be wrong. An accurate sizes attribute gives the browser a true picture of how large the image actually renders, instead of the viewport-width default; candidates generated around real display widths ensure there's a genuinely appropriately-sized option to choose from; a properly configured framework component keeps that information accurate automatically as layouts change; and fixing the upload pipeline ensures smaller image variants exist to select in the first place, which no amount of correct markup can substitute for.
Conclusion
A responsive image downloading an oversized file isn't a browser bug or a broken srcset — the browser is selecting correctly from what it's told, and what it's told is inaccurate. Set a sizes attribute that matches the image's real rendered width at each breakpoint, generate srcset candidates around the container's actual display sizes rather than a generic set, use a framework's image component with accurate layout props where available, and make sure the upload pipeline actually produces smaller variants for the browser to choose from.
