On this page 10 sections
- One image file is rarely the right size
- srcset, sizes and the slot the image will fill
- Width descriptors versus density descriptors
- Art direction with picture
- Formats, compression and what to measure
- Lazy loading, fetch priority and LCP images
- How to check what the browser actually requested
- A default for articles, cards and full-bleed heroes
- Key takeaways
- Frequently asked questions
I generate srcset and sizes for images on leej.uk from the real layout slot, and I built the srcset Generator and the srcset sizes Calculator so I could stop guessing the attributes. The method below is how I write them, how I treat the LCP image, and how I check what the browser actually requested.
One image file is rarely the right size
A hero that is 1,280 pixels wide on a desktop, 840 on a tablet, and the full viewport on a phone is three different download problems, even before you count device pixel ratio. A 2x phone in a 320 CSS-pixel slot still only needs a file around 640 pixels wide, not the original 3,000-pixel export from a camera.
If you ship one large file:
- People on a narrow connection pay for pixels they will never see.
- The Largest Contentful Paint can be dominated by that download, which is a Core Web Vitals problem as well as a bandwidth one.
- People on a large, dense display can still see a soft image if you over-corrected and shipped only a small file.
The HTML for responsive images exists so you can offer a set of candidates and describe the slot. Compression, which the Image Compressor and the Image Quality Explorer help with, is the other half. You need both: a well-compressed file of the wrong width is still the wrong file.
This is the same instinct as designing for the Apple Watch or any small screen: decide the size the image will actually occupy, then supply pixels for that size, not for the largest device you can imagine.
srcset, sizes and the slot the image will fill
For a flexible image, the usual pattern is a srcset of width descriptors and a sizes attribute that describes the slot:
<img
src="/images/harbour-800.jpg"
srcset="
/images/harbour-400.jpg 400w,
/images/harbour-800.jpg 800w,
/images/harbour-1200.jpg 1200w,
/images/harbour-1600.jpg 1600w"
sizes="(min-width: 1024px) 640px, 92vw"
width="1600"
height="900"
alt="Whitby harbour at low tide, looking towards the abbey.">srcsetlists candidate files and the width of each file, in pixels, as awdescriptor. Those numbers are the intrinsic width of the file, not the CSS width in the layout.sizesis a list of media conditions and, for each, the width of the slot in CSS pixels (or a length such as92vw). The browser uses this before layout is finished, so it can start the download early. It will not measure the live CSS for you at that moment.srcis the fallback file for older clients, and a reasonable default ifsrcsetis ignored. It should be a mid-sized, usable image, not a 50-pixel placeholder and not the original camera file.widthandheight(or a CSS aspect-ratio) reserve space and help avoid layout shift. They should match the aspect ratio of the file, not an arbitrary box you hope to squeeze later.
sizes is the attribute people invent. If the desktop layout actually shows the image at 640 CSS pixels, write 640px for that breakpoint, not 100vw. If on smaller screens the image is almost the full viewport plus a page margin, measure that margin and write something like 92vw, rather than 100vw out of habit. The srcset sizes Calculator is a way to turn a layout into that string. The srcset Generator will then pair it with a set of file widths.
The HTML specification and MDN’s img documentation are the references for the syntax. The important idea is: srcset is supply, sizes is demand.
Width descriptors versus density descriptors
You will also see density descriptors (1x, 2x, 3x):
<img
src="/images/icon-32.png"
srcset="/images/icon-32.png 1x, /images/icon-64.png 2x"
width="32"
height="32"
alt="">This is the right tool when the image’s CSS size is fixed: an icon, a logo in the header, an avatar at 48 CSS pixels. The browser picks by device pixel ratio. You do not need sizes, and you should not mix x and w descriptors on the same srcset.
For everything that stretches with the layout — heroes, article photographs, cards, full-bleed images — use width descriptors and sizes. A 2x pair cannot describe “this is half the viewport on a phone and 640 pixels on a desktop”. If you only offer 1x and 2x of a desktop-sized file, a phone still has to choose among files that are too wide.
A reasonable set of widths is a geometric series around the slots you actually have, for example 400, 800, 1,200 and 1,600 for an article photo, not twelve barely different copies. Produce them from the same master, and keep the aspect ratio identical unless you are doing art direction.
Art direction with picture
srcset on one <img> is for the same picture at different resolutions. When the picture itself should change — a cropped portrait on a phone, a wide landscape on a desktop, or a different focal point — use <picture> with media conditions:
<picture>
<source
media="(max-width: 599px)"
srcset="/images/harbour-tall-400.jpg 400w, /images/harbour-tall-800.jpg 800w"
sizes="92vw">
<source
media="(min-width: 600px)"
srcset="/images/harbour-wide-800.jpg 800w, /images/harbour-wide-1600.jpg 1600w"
sizes="(min-width: 1024px) 640px, 92vw">
<img
src="/images/harbour-wide-800.jpg"
width="1600"
height="900"
alt="Whitby harbour at low tide, looking towards the abbey.">
</picture>The last element must be an <img>, with alt, and preferably with width and height. It is the fallback and the element that CSS hooks onto. The <source> elements offer alternative files; they are not a substitute for the img.
<picture> is also how you offer modern formats with a fallback:
<picture>
<source type="image/avif" srcset="/images/harbour-800.avif 800w, /images/harbour-1600.avif 1600w" sizes="(min-width: 1024px) 640px, 92vw">
<source type="image/webp" srcset="/images/harbour-800.webp 800w, /images/harbour-1600.webp 1600w" sizes="(min-width: 1024px) 640px, 92vw">
<img
src="/images/harbour-800.jpg"
srcset="/images/harbour-800.jpg 800w, /images/harbour-1600.jpg 1600w"
sizes="(min-width: 1024px) 640px, 92vw"
width="1600"
height="900"
alt="Whitby harbour at low tide, looking towards the abbey.">
</picture>Keep sizes consistent across the sources, and keep the art-direction crops consistent with the alt text. If the mobile crop no longer shows what the alt describes, the alt is wrong, or the crop is. That is an accessibility issue as well as a layout one, in the same spirit as accessible interfaces: the meaning has to survive the format.
Formats, compression and what to measure
AVIF and WebP often shrink a photograph compared with JPEG, at a similar visual quality, and they are widely supported in current browsers. They are not a reason to skip srcset. A 2,000-pixel AVIF in a 360-pixel slot is still a waste. Offer a set of widths in the modern format, and a JPEG (or PNG, for graphics that need it) fallback in the img.
Measure the result, not the rumour of a format:
- Compress each width until it still looks acceptable at the size it will be shown. The Image Quality Explorer is useful here. A hero can bear a higher quality than a small card thumbnail.
- Compare transferred bytes at the actual width, with the Image Compressor as a starting point, then confirm on the wire.
- SVG remains the right choice for many logos and icons. Do not rasterise a simple mark into five PNG widths if a clean SVG will do, and do not inline a huge SVG illustration without looking at its size.
- Do not automatically convert every image on the site in one pass and then fail to check a chart, a screenshot with text, or a graphic with hard edges. Photographs and UI captures want different settings.
A performance budget should cap the LCP image and the typical article’s image bytes, so a new hero cannot walk in at four megabytes because “AVIF is efficient”.
Lazy loading, fetch priority and LCP images
loading="lazy" tells the browser it may defer images that are not yet near the viewport. That is appropriate for images further down an article, and it is usually wrong for the LCP image: the hero, the first large photograph, the main product shot. Lazy-loading that element can delay the largest paint on purpose. Leave the LCP image eager (the default), and consider fetchpriority="high" on it so the browser knows it competes well with other early requests.
<img
src="/images/harbour-800.jpg"
srcset="..."
sizes="..."
width="1600"
height="900"
fetchpriority="high"
alt="Whitby harbour at low tide, looking towards the abbey.">Below-the-fold images can take loading="lazy" and, if you like, a lower fetch priority. Do not lazy-load images that are already in the first viewport on a common phone width; test at a narrow width, not only on a tall desktop window.
decoding="async" can help the browser decode without blocking the main thread; it is a modest extra, not a substitute for a correctly sized file.
Preload (<link rel="preload" as="image">) is sometimes useful for the LCP image, but if you preload you must preload the file the browser will actually pick, including format and width, or you can download twice. A well-written srcset and sizes in the first HTML, with fetchpriority="high", is the simpler default. If you do preload, keep it in sync with the markup, and put the <img> in the first response, not only after JavaScript, or a crawler and a cold load will not see it in time.
How to check what the browser actually requested
Do not trust the markup alone.
- Open the Network panel, filter to images, and load the page at the viewport you care about, with caching disabled for the test, on a representative pixel ratio. Note the file that was requested, its dimensions, and its transfer size.
- In the console,
document.querySelector('img.hero').currentSrc(or the appropriate selector) shows the chosen URL after the decision. - Change the viewport and reload, or use a device toolbar, and confirm a smaller slot requested a smaller width. Resizing alone may not always pick a new candidate if a larger file is already cached; a reload on a clear cache is the fair test.
- Compare the chosen file’s intrinsic width with
devicePixelRatio * slot width. If you are consistently downloading a file twice as wide as that,sizesis probably too large, or yoursrcsetjumps too aggressively. - For the LCP image, confirm in a performance trace or Lighthouse that the LCP element is that image, that it was not lazy-loaded, and that it was discovered in the first HTML. This is the same “look at the recording” habit I use when finding performance problems with Instruments.
If the browser picked a surprisingly large file, fix sizes before you add more widths. Extra candidates will not help a demand string that claims the image is the full viewport when it is not.
A default for articles, cards and full-bleed heroes
These are the defaults I start from, then I measure.
An article photograph in a column that is, say, 640 CSS pixels on a desktop and almost full-width on a phone: one <img> with width-descriptor srcset (for example 400, 800, 1,200, 1,600), a sizes string that matches that column, width and height, a mid-sized src, and loading="lazy" only if the image is definitely below the fold. Offer AVIF or WebP via <picture> if you already have those derivatives.
A card image in a grid of two or three columns: smaller slots, so smaller widths, and a sizes string that divides the viewport (for example (min-width: 1024px) 300px, (min-width: 640px) 45vw, 92vw). Card images are where a copied hero sizes="100vw" does the most damage, because you may have several of them.
A full-bleed hero that is also the LCP element: widths up to the largest layout you support, a sizes of 100vw if it really is full bleed, no lazy loading, fetchpriority="high", reserved space, and the markup in the first HTML response. If the hero crops differently on a phone, use <picture> art direction, and keep the reserved aspect ratio from jumping.
Icons and logos at a fixed CSS size: density descriptors, or a single SVG. Do not run them through the same 1,600-pixel pipeline as a photograph.
Write alt for all of these as if the file failed to load, which it sometimes will.
Key takeaways
srcsetis the list of candidate files;sizesis the width of the layout slot. The browser needs both, andsizeshas to match the real CSS, not a guess of100vwon every image.- Use width descriptors for flexible photographs, and density descriptors only for fixed-size icons. Do not mix the two on one
srcset. - Use
<picture>for art direction and for modern formats with a fallback. A new format withoutsrcsetwill still send the wrong width. - Do not lazy-load the LCP image. Give it
fetchpriority="high", reserved dimensions, and a place in the first HTML response. - Check
currentSrcand the Network panel at real viewports, and cap hero and article image bytes in a performance budget so a new file cannot ignore the slot.
If you want a second pair of eyes on a hero that is still two megabytes on a phone, tell me about it.
Frequently asked questions
What is the difference between srcset and sizes?
srcset lists the files you have, and the width (or density) of each file. sizes tells the browser how wide the image will be in the layout at different viewport widths, so it can choose among those files before layout is complete. Supply and demand, respectively. One without a careful other still leads to an oversized download.
Should I use width descriptors or pixel density descriptors?
Use width descriptors (800w) plus sizes for images that change width with the layout. Use density descriptors (1x, 2x) for images that stay a fixed CSS size, such as icons. For heroes, article photographs and cards, width descriptors are the right default.
How do I write a sizes attribute that is not a guess?
Measure the CSS width of the slot at your breakpoints, including margins and grid columns, and write those lengths into sizes. A calculator helps, but it still needs the real layout as input. Then confirm, in the Network panel, that a narrow viewport requests a smaller file than a desktop viewport.
When should I use the picture element?
Use it when you need art direction (a different crop or image at a breakpoint) or when you want to offer AVIF or WebP with a JPEG or PNG fallback. The last child should still be an img element with alt text and dimensions. If you only need resolution switching of the same picture, srcset and sizes on the img are enough.
Is WebP or AVIF enough without srcset?
No. A modern format can reduce bytes for a given width, but it does not choose a width. Without srcset (and a correct sizes), the browser still has only one file, which is often the desktop original. Offer several widths in the modern format, and keep a fallback in the img.
Written by Lee Johnson. Published 10 November 2026.