In short and in plain language
An image often comes in several sizes. The browser should load the right size. For this, it needs to know how wide the image will be on the page.
If this is missing, the browser counts with the full width of the window. Then it often loads a file that is far too big. This makes the page slower.
The value must fit the layout of the page. If the layout changes, you have to check the value again.
You can see which file the browser really loads. You can do this with the tools in the browser.
We check the images on your pages. Then we adjust the values. This way, the browser usually stops loading images that are too big.
The product images exist in several sizes, the srcset is well maintained, and yet the narrow tile in the listing loads a file meant for the full screen width. The analysis then reports oversized images, formerly under the heading “Properly size images”. The cause rarely lies in the srcset itself but in the attribute next to it: sizes is missing or describes a grid that no longer exists in that form after the last theme change. This article shows how the browser picks a file from sizes and pixel density, how to derive sizes from the grid for each template and how to prove in the developer tools which file was actually loaded.
Key takeaways
- Without
sizes, the browser assumes the full window width of 100vw (HTML Living Standard). On desktop, 1 in 5sizesattributes in 2024 was inaccurate enough to make the browser pick an unsuitable file (Web Almanac 2024). - The browser takes the first matching condition in
sizes(MDN Web Docs), divides the w descriptors insrcsetby that width and matches the result against the pixel density. An error insizesis therefore multiplied by the pixel density. - Derive
sizesper template from the grid: column count, gap, margin and maximum container width at each breakpoint. After a theme change, fixed values need review, in Shopware for example those of the listing tile (Shopware source code). - What counts is the loaded file: compare
currentSrcand the real file width with displayed width times pixel density, plus a network check without cache. Since version 13, Lighthouse checks image sizes in the Improve image delivery insight (Chrome for Developers). -
sizes="auto"only applies to images withloading="lazy"and is supported from Chrome 126, Firefox 150 and Safari 27 with limitations (MDN browser-compat-data). The LCP image therefore still needs an explicitsizeslist. - Before Black Week, measure every new promo tile at mobile and desktop width: the first visible tile loads eagerly with an explicit
sizes, tiles further down load lazily withsizes="auto"and a fallback list.
Why the large file loads despite srcset
The browser requests images very early, often before a stylesheet has been loaded or applied. At that point it only knows the markup, the size of the window and the pixel density of the screen, but not the layout of the page (web.dev). How wide a tile will end up is defined in the CSS, which is not available yet. That is exactly what sizes is for: the attribute describes how wide the image will appear in the layout, so the browser can pick the right file from srcset while it is still reading the markup.
If sizes is missing, the browser assumes the full window width: the parsing step in the HTML standard ends by returning 100vw (HTML Living Standard). For a listing tile that takes up a quarter of the width, that is a multiple of the actual space. A worked example: the tile is 300 pixels wide, the window 1440 pixels, and the screen has a pixel density of 2. With a matching sizes, the browser needs a file about 600 pixels wide. Without sizes, it looks for a file for 2880 pixels and takes the largest one the srcset offers. The HTML standard explicitly requires sizes as soon as srcset contains width descriptors.
The HTTP Archive Web Almanac shows how common the problem is. 42 percent of mobile pages used the srcset attribute in 2024 (Web Almanac 2024). For 62 percent of srcset values on mobile pages and 64 percent on desktop, files are described by their width, using w descriptors (Web Almanac 2024). These are exactly the descriptors that depend on sizes. On desktop, 1 in 5 sizes attributes was inaccurate enough to make the browser pick an unsuitable file from srcset; on mobile, this applied to 14 percent (Web Almanac 2024).
On desktop the error is more expensive, because the gap between the full window width and the real image width is larger there. The Almanac estimates that a quarter of desktop pages using w descriptors load 180 KB or more of excess image data, and the worst 10 percent of these pages close to a megabyte (Web Almanac 2024). The measurements used a desktop crawler 1376 pixels wide with a pixel density of 1 and a mobile crawler 360 pixels wide with a pixel density of 3 (Web Almanac 2024). Real devices differ, but the order of magnitude shows where a check pays off.
That is to say, a better, smaller resource is there for the picking in the srcset, but because the sizes attribute is so erroneous, the browser doesn’t pick it.
How the browser chooses from sizes and pixel density
Selection happens in three steps. First, the browser goes through the list in sizes from left to right and takes the first media condition that matches; the entry without a condition at the end applies if none matches (MDN Web Docs). This width is the space the image gets in the layout. Next, it divides every w descriptor in srcset by that width. With a space of 400 pixels, 800w becomes an effective density of 2. Finally, it compares these densities with the pixel density of the device and loads the file that fits most closely.
Because the first matching condition wins, order matters. MDN puts it plainly: the browser ignores everything after the first matching condition (MDN Web Docs). If you write (min-width: 576px) 50vw before (min-width: 1200px) 300px, desktop gets half the window width, because the first condition matches there as well. With min-width, the widest condition goes first; with max-width, the narrowest.
The following table works this through for a listing tile with a srcset of four steps: 400, 600, 1000 and 1600 pixels. It is a worked example; the browser may decide differently in individual cases, as the next section shows.
| Situation | Width per sizes | Pixel density | Width needed | File chosen |
|---|---|---|---|---|
Desktop, sizes missing | 1440 px (100vw) | 1 | 1440 px | 1600w |
Desktop, sizes missing | 1440 px (100vw) | 2 | 2880 px | 1600w, largest step |
Desktop, sizes matches | 300 px | 1 | 300 px | 400w |
Desktop, sizes matches | 300 px | 2 | 600 px | 600w |
Smartphone, sizes missing | 390 px (100vw) | 3 | 1170 px | 1600w |
Smartphone, two columns, sizes matches | 180 px | 3 | 540 px | 600w |
Pixel density is the part many underestimate. A file twice as wide has four times as many pixels, and the file size grows with it. In the MDN example, the same motif weighs 93 KB at 640 pixels wide and 39 KB at 320 pixels (MDN Web Docs). That is why sizes matters so much on high-resolution smartphones: every error in the stated width is multiplied by the pixel density, and a small slip in the markup turns into a much larger file.
The standard leaves the browser room to choose. Besides pixel density, it may take zoom level and network conditions into account (HTML Living Standard), and the actual selection step is explicitly left to the implementation (HTML Living Standard). In practice, this means: if a larger version of the same image is already in memory, the browser may reuse it. In a test with Chrome, a lazy-loaded image with sizes="auto" loaded the large file in most runs, because the same motif appeared further up without sizes and had already loaded the large version; on its own, the same image loaded the matching small file. A missing sizes in one place can therefore drag a second place along with it.
Common causes: sizes no longer matches the grid
A sizes attribute is a description of the layout that lives in the markup, not in the CSS. When the layout changes, the description does not change with it. In 2024, the median sizes attribute was 16 percent too large on mobile and 43 percent too large on desktop, and the values had become worse since the previous analysis (Web Almanac 2024). In shops, we mainly come across six causes.
-
sizesis missing entirely. This often happens in custom templates, plugins and page building blocks that generate asrcsetbut nosizes. In Shopware, the thumbnail function generates nosizesif the call passes neither sizes nor a column count (Shopware source code). The browser then calculates with 100vw. - Grid drift after a theme change. Fixed pixel values only fit the grid they were written for. Shopware stores fixed values for the product tile in the listing, from 500 pixels on narrow screens to 280 pixels on wide ones (Shopware source code). Anyone who changes the column count in the theme, removes the filter sidebar or widens the tile has to update these values too.
- Image elements using the original width. In the “Standard” display mode, the image element of shopping experiences passes the width of the uploaded original file as
sizes, while the stylesheet limits the display to the column width (Shopware source code). If the original is wider than the column, the browser picks a larger step than necessary. - Missing container limits.
sizesis often written in vw only. On wide monitors the value keeps growing, even though the content area stops at a maximum width. From that width on, the list needs a fixed pixel value or a cap withmin(), since math functions are allowed insizes(HTML Living Standard). - Sliders and horizontal strips. In horizontally scrolling areas, the window width is no upper bound for the image width (web.dev). Cross-selling strips and promo sliders therefore need their own value at each breakpoint.
- Wrong order of conditions. With
min-width, the widest condition has to come first. If it sits at the end, it is not reached, and large screens get the value meant for medium ones.
The product gallery follows its own rules; image steps and sizes per gallery element are covered in the article on the product gallery. This article is about the places where many similar images sit side by side: listing, shopping experiences, promo tiles and magazine. Maintaining these values is part of front-end optimization rather than image editing, because the image files themselves are usually fine.
Derive sizes per template from the grid
A matching sizes does not come from trial and error but from the grid of the template. At each breakpoint, you need four values: the column count, the gap between columns, the side margin and the maximum width of the content area. Together they give the width of one column, and exactly this width goes into sizes as an expression. If you already group your pages by template, as described in the article template performance beyond the home page, you almost have the list of image slots together. For a listing tile, the result looks like this:
<!-- Listing tile: 2 columns below 768 px (margin 16 px, gap 12 px),
3 columns from 768 px and 4 from 1200 px (margin 24 px, gap 24 px),
content area at most 1320 px wide -->
<img
src="/media/jacket-600.avif"
srcset="/media/jacket-400.avif 400w,
/media/jacket-600.avif 600w,
/media/jacket-1000.avif 1000w"
sizes="(min-width: 1368px) 312px,
(min-width: 1200px) calc((100vw - 120px) / 4),
(min-width: 768px) calc((100vw - 96px) / 3),
calc((100vw - 44px) / 2)"
width="600" height="600" loading="lazy" decoding="async"
alt="Hiking jacket in blue">Two details make the difference. First, sizes may be slightly larger than the real width, but not much smaller: a value that is too small leads to a file the browser upscales, and the image looks blurry. The Almanac explicitly calls sizes a hint where a little inaccuracy is fine (Web Almanac 2024). Second, an expression with calc() follows the window smoothly, while a fixed pixel value is inevitably too large or too small between two breakpoints. Fixed values are therefore mainly suited to widths above the maximum container width.
| Template and image slot | Grid | Approach for sizes | What to watch |
|---|---|---|---|
| Home page, full-width header image | one column, edge to edge | 100vw | LCP image: load eagerly, set sizes explicitly |
| Listing, product tile | two to five columns per breakpoint | calc() per breakpoint, fixed value above container width | check after every change to column count or filter sidebar |
| Shopping experience with two or three image columns | columns in the content area | column width from container width and gap | check the display mode of the image element |
| Promo tile, campaign block | often its own grid | separate list per block | measure in the layout before the campaign starts |
| Cross-selling strip, slider | horizontally scrolling | tile width per breakpoint | window width is no upper bound |
| Magazine, image in body text | text column with max width | min(100vw - 32px, 760px) | take the max width from the CSS |
For Shopware themes, this means: the values for the listing tile sit in a Storefront template that a custom theme can override. Whoever changes the grid adjusts these values in the same step instead of inheriting them from the default. Further levers in the theme are summarized on the Shopware performance page.
Checking which file the browser really loads
Whether sizes is right is shown not by the source code but by the loaded file. MDN recommends the network panel of the developer tools for this and advises disabling the cache in Chrome so the browser does not fall back on images it has already downloaded (MDN Web Docs). Filter the list to images and reload the page at several window widths. How to read the log line by line is explained in the article on the network waterfall.
The currentSrc property is faster. It returns the URL of the image the browser selected from the srcset (MDN Web Docs). There is a trap in naturalWidth: the value is corrected by the pixel density of the selected source (HTML Living Standard) and, with w descriptors, does not show the pixel width of the file. The following snippet therefore loads the chosen file a second time without srcset and compares its real width with the need derived from display width and pixel density.
// Run in the developer tools console.
// Compares per image: displayed width times pixel density against the width of the loaded file.
const images = [...document.images].filter(img => img.currentSrc && img.clientWidth > 0);
const rows = await Promise.all(images.map(img => new Promise(resolve => {
const probe = new Image(); // without srcset: naturalWidth is the real file width here
probe.onload = probe.onerror = () => {
const needed = Math.round(img.clientWidth * devicePixelRatio);
resolve({
file: img.currentSrc.split('/').pop().slice(0, 50),
shownPx: img.clientWidth,
neededPx: needed,
filePx: probe.naturalWidth,
factor: Number((probe.naturalWidth / needed).toFixed(2)),
sizes: img.getAttribute('sizes') || '(missing: 100vw)'
});
};
probe.src = img.currentSrc;
})));
console.table(rows.sort((a, b) => b.factor - a.factor));The factor column tells you how much wider the file is than necessary. A value just above 1 is normal, because the browser takes the next larger step. If the factor is clearly above the gap between two steps in the srcset, sizes is wrong or missing. Repeat the measurement in device emulation with pixel densities of 2 and 3 and at the window widths of your breakpoints. Lazy-loaded images only appear in the table once you have scrolled to them.
Lighthouse and PageSpeed Insights check image size too. The former audit “Properly size images” has moved into the Improve image delivery insight as of Lighthouse 13 (Chrome for Developers). The old audit compared the displayed size, including pixel density, with the size of the loaded file (Chrome for Developers). The insight compares the download size with an efficient bytes-to-pixel ratio for the format and only flags images with estimated savings of at least 4 KiB (Chrome for Developers). It therefore also fires on weak compression or an unfavorable format, which have nothing to do with sizes. How to rank such findings by savings is shown in the article on reading a Lighthouse report; a Lighthouse audit does this ranking for your templates.
- Network panel with cache disabled, filtered to images
- Measurement at every window width where the grid breaks
- Device emulation with pixel densities 1, 2 and 3
- Compare
currentSrcand real file width with displayed width times pixel density - Check images without
sizesand images with a plain vw value separately - Record the result per template, not just for the home page
sizes="auto": a shortcut for lazy images with a fallback
For images outside the first screen there is now a shortcut. With sizes="auto", the browser takes the width from the computed layout instead of reading it from a list; this only applies to images with loading="lazy" (Web Features Explorer). The HTML standard requires auto to be the first entry and allows a fallback list after it for browsers without support (HTML Living Standard). Without width and height or a width in CSS, such an image will likely render at 300 × 150 pixels (HTML Living Standard).
<!-- Tile further down: width from the layout, list as fallback -->
<img loading="lazy" width="600" height="600"
sizes="auto,
(min-width: 1368px) 312px,
(min-width: 1200px) calc((100vw - 120px) / 4),
(min-width: 768px) calc((100vw - 96px) / 3),
calc((100vw - 44px) / 2)"
srcset="/media/jacket-400.avif 400w, /media/jacket-600.avif 600w, /media/jacket-1000.avif 1000w"
src="/media/jacket-600.avif" alt="Hiking jacket in blue">According to MDN browser-compat-data, auto is supported in Chrome from version 126, in Firefox from version 150 and in Safari from version 27, there with a limitation for SVG images with object-fit (MDN browser-compat-data). Some overview pages still list Safari without support; with a fallback list, the difference does not matter in practice, because older versions read the list after auto. For the LCP image, auto does not help, because it should not load lazily; it still needs an explicit list. The Almanac recommends exactly this: fix critical images and use auto for deferred images (Web Almanac 2024). How this looks in the thumbnail strip of a product page is also covered in the article on the product gallery.
Promo tiles for Black Week: check image sizes before launch
Shortly before Black Week, many shops add new image areas: promo tiles on the home page, campaign blocks in categories, banners above the listing. They are often built at short notice, uploaded in full resolution and placed in building blocks whose grid rarely anyone recalculates. This is exactly where the error from this article appears in its most expensive form.
A promo tile in the first screen is often the LCP element. If its sizes is too large, the load time of the largest element grows by the excess bytes. If it is also marked as lazy, the download only starts after layout. How to reserve space for banners and countdowns so nothing jumps is described in the article on promo banners for Black Week. What else matters for fast campaign pages is summarized on the PageSpeed for online shops page.
- Measure every new tile once with the snippet above, at mobile and at desktop width.
- For the first visible tile, set
loading="eager",fetchpriority="high"and an explicitsizeslist; details in the article on fetchpriority. - For tiles further down, use
loading="lazy"andsizes="auto"with a fallback list. - Provide campaign images in several steps instead of only at original size.
- After building, check whether the same motif appears elsewhere without
sizesand drags the large file along.
What a fix delivers and where its limits are
A matching sizes saves transfer without making any image look worse. How much depends on the starting point. By the Almanac estimate, a quarter of desktop pages using w descriptors load 180 KB or more in excess, while at least half of these pages load nothing in excess (Web Almanac 2024). Whether your shop belongs to the first or the second group is shown by a measurement per template.
The fix has limits where the problem is not the width. Format and compression are a separate matter; more on that in the article on WebP and AVIF. Nor does sizes change the loading order: an LCP image that is discovered late or loaded lazily stays late, even if the file is small. And whether the improvement reaches real visitors only shows in field data, for example in the Core Web Vitals report in Search Console.
In an image optimization project, we follow exactly this path: we list the image slots per template, measure loaded against needed width at several window widths and pixel densities, derive sizes from the grid and measure again afterwards. In Shopware shops, we adapt the theme templates for this. If you first want to know where your shop stands, it starts with an analysis of the most important templates. Which metrics matter here is explained on the Core Web Vitals page.
Sources and documentation
sizes is missing or no longer matches the layout. Without sizes, the browser calculates with the full window width (HTML Living Standard) and picks a file for that width, multiplied by the pixel density. A second cause: the same file has already been loaded in a large version elsewhere on the page, and the browser may reuse it. A measurement with the cache disabled separates the two cases.sizes as soon as srcset contains width descriptors. Browsers do handle a missing sizes, but then assume 100vw (HTML Living Standard). For images that span the full width, that is correct; for tiles, columns and images in body text, it leads to oversized files.sizes is a hint, and a small deviation is explicitly expected (Web Almanac 2024). It becomes critical when the value is so far off that the browser picks a different step from srcset. Slightly too large is better than too small, because a value that is too small leads to an upscaled, blurry image. For checking, comparing loaded and needed width at the breakpoints is enough.auto only works for images with loading="lazy" (HTML Living Standard). The LCP image and other images in the first screen should not load lazily and therefore need an explicit list. For images further down, auto with a fallback list makes sense: Chrome from 126, Firefox from 150 and Safari from 27 with limitations support it (MDN browser-compat-data), older versions use the list after it.sizes helps, in the second format and compression.sizes from the column count of the block, unless the image element is in the “Standard” display mode; then it uses the original width of the file (Shopware source code).Related Articles
Reading the network waterfall: what each row reveals
Learn to read the network waterfall: how it is ordered, what the phases of a bar show and which six patterns across rows reveal typical slowdowns.
WordPress Slow Despite Caching: WP-Cron and admin-ajax
Page cache on, WordPress still sluggish? WP-Cron, Heartbeat and admin-ajax bypass the cache and tie up PHP workers. How to measure that load and take it off.
Less JavaScript: Dialogs and Popovers Straight in HTML
Dialogs, menus and filter drawers without custom scripts: how command and commandfor work, which fallback older browsers need and what it all means for INP.