Oğuzhan KARAGÜZEL

OĞUZHAN KARAGÜZEL

Software Developer

The sizes Attribute: Why Browsers Download Images Too Large

Author: Oğuzhan KARAGÜZEL ·

You gave an image several sizes with srcset and assumed the browser would pick the one that fits the screen. Then you opened DevTools and saw a 2400-pixel file downloaded for a 400-pixel card image. Most of the time the reason is a missing or wrong sizes attribute. In this article I explain how the browser chooses an image size, how to write sizes correctly, why grid layouts make it harder, and when sizes="auto" helps.

How does the browser choose an image?

Multiple sizes of an image are given with width descriptors (w):

HTML
<img src="/img/card-800.jpg"
     srcset="/img/card-480.jpg 480w,
             /img/card-800.jpg 800w,
             /img/card-1200.jpg 1200w,
             /img/card-2400.jpg 2400w"
     width="1200" height="630" alt="...">

The browser decides which one to download before it computes the page layout. That is a deliberate choice for speed: the image request starts while the HTML is being parsed, before the CSS has even arrived. But it also means the browser doesn't know, at that moment, how wide the image will be on screen. Telling it is the job of the sizes attribute.

Without sizes, the browser assumes the image will be as wide as the whole viewport (100vw). It then picks the smallest candidate that is at least the display width × the screen's pixel density.

What happens without sizes? A worked example

Take a laptop screen 1920 pixels wide with a pixel density of 2, and a content area at most 1200 pixels wide. That area holds three cards side by side, each image about 400 CSS pixels wide.

  • What is actually needed: 400 × 2 = 800 pixels. card-800.jpg is enough.
  • What the browser assumes without sizes: 1920 × 2 = 3840 pixels. So the largest candidate, card-2400.jpg, is downloaded.

The result is about nine times the pixels each card needs, and the file size grows roughly in proportion. In a three-card grid, on a mobile connection, that difference turns into seconds and directly lengthens the time it takes to load the page's largest content (LCP).

Writing sizes correctly

sizes is a comma-separated list of "condition + width" pairs. The browser uses the first matching condition from left to right; the last value has no condition and applies when none match:

HTML
<img srcset="..."
     sizes="(max-width: 768px) 100vw,
            (max-width: 1200px) 33vw,
            400px"
     ...>

Read it like this:

  • On screens narrower than 768 pixels the cards stack, and the image is as wide as the screen.
  • Up to 1200 pixels there are three columns, and the image is a third of the screen.
  • On wider screens the content area stops at 1200 pixels, and the image is at most 400 pixels.

A few rules make life easier:

  • You don't have to be exact. sizes is an estimate. If you ignore gaps and padding, the browser picks at most one size larger; it never shows a blurry image. If you are going to be wrong, be wrong on the large side.
  • Match the conditions to your CSS breakpoints. If the grid drops to one column at 768 pixels, the condition in sizes should be 768 too.
  • Use vw or pixels, not percentages. Percentages are invalid in sizes; calc() works: calc((100vw - 48px) / 3).
  • Don't forget width and height. They don't affect which file is downloaded, but they let the browser reserve the image's space in advance and prevent layout shift (CLS).

The hard part in grids: auto-fit

With a fixed number of columns, sizes is easy. But modern card layouts are often written like this:

CSS
.cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 24px;
    max-width: 1200px;
}

Here the number of columns isn't set by the CSS but by the screen width: a 1200-pixel area fits three columns of at least 320 pixels, a 700-pixel area two, a phone one. The card width changes with the column count. A reasonable sizes for this layout gives the largest width a card can reach in each range:

HTML
sizes="(max-width: 700px) 100vw,
       (max-width: 1040px) 50vw,
       400px"

These values are upper bounds, not exact figures, but they are far more accurate than the 100vw assumption. In Elevare, the platform this site runs on, we don't do this by hand; the server does it. While rendering, it reads the page's own CSS, finds the column count of the grid the image sits in, or the minimum column width in minmax(), and the container's max-width, and writes sizes from that. The estimate is an upper bound; since gaps are ignored, the browser may pick one size larger when needed, never a smaller one.

sizes="auto": leaving the estimate to the browser

For lazy-loaded images there is now a simpler way. A lazy image's request only starts after layout, when the image approaches the viewport. At that point the browser knows the image's real width. sizes="auto" tells the browser to "use the width you measured instead of an estimate":

HTML
<img loading="lazy"
     srcset="..."
     sizes="auto, (max-width: 700px) 100vw, 400px"
     width="1200" height="630" alt="...">

Two things to watch:

  • It only works with loading="lazy". Images on the first screen shouldn't be lazy (that delays LCP), so you still have to write sizes by hand for them.
  • Leave a fallback. sizes="auto" is supported in Chromium-based browsers for now. A browser that doesn't support it skips auto and uses the values after the comma, so always follow auto with an estimate rather than writing it alone.

Together with WebP: picture and source

If you also serve the image as WebP or AVIF, you use <picture>. Each <source> carries its own srcset, and sizes must be repeated on each of them too; the sizes on the <img> doesn't carry over to the sources:

HTML
<picture>
  <source type="image/webp"
          srcset="/img/card-480.webp 480w, /img/card-800.webp 800w, /img/card-1200.webp 1200w"
          sizes="(max-width: 700px) 100vw, 400px">
  <img src="/img/card-800.jpg"
       srcset="/img/card-480.jpg 480w, /img/card-800.jpg 800w, /img/card-1200.jpg 1200w"
       sizes="(max-width: 700px) 100vw, 400px"
       width="1200" height="630" alt="..." loading="lazy">
</picture>

A common mistake is to write sizes only on the <img>. Since every current browser that supports WebP uses the source, the file actually downloaded is still chosen with the 100vw assumption.

How do you know the right file was downloaded?

The quickest way is to check every image on the page with one line in the browser console:

JavaScript
[...document.images].map(img => ({
  file: img.currentSrc.split('/').pop(),
  downloaded: img.naturalWidth,
  needed: Math.round(img.clientWidth * devicePixelRatio)
}));

If downloaded is several times needed, that image's sizes is missing or wrong. One size larger is normal; the steps between candidates make it unavoidable.

A note: Lighthouse's "Improve image delivery" warning calculates savings against the image's CSS size on screen and doesn't account for pixel density. So the warning can persist even for an image chosen correctly for a 2x screen. Rather than trying to silence the warning blindly, use the check above to see whether extra data is really being downloaded.

Conclusion

srcset on its own is half a solution; without sizes telling the browser how wide the image will be, the largest file is often downloaded. In fixed-column layouts, write sizes to match your CSS breakpoints; in auto-fit grids, give an upper bound for each range; for lazy images, use sizes="auto" together with a fallback estimate; and don't forget sizes on every source inside <picture>. Resource Hints, on image download priority, and Website Speed and Core Web Vitals, for an overview, may also help.