HTML3 min read

srcset and sizes: the part everyone gets wrong

srcset alone does not save bandwidth. Without a correct sizes attribute the browser assumes your image is the full page width and downloads the largest file anyway.

You add srcset with four widths, deploy, and the network panel still shows the 1600px file on a phone. Nothing improved.

The missing piece is sizes, and the default value is the trap.

The mental model

The browser picks an image before it has done layout. It does not know your CSS yet. So it needs you to tell it, in advance, roughly how wide the image will be on screen.

That is what sizes is. When you leave it out, the browser assumes 100vw, which means "as wide as the viewport". On a 400px phone with a 2x screen that is 800 device pixels, so it picks the 800px file or larger, even if the image is displayed in a 150px thumbnail.

The width descriptor version

<img
  src="photo-800.jpg"
  srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w, photo-1600.jpg 1600w"
  sizes="(min-width: 1024px) 360px, (min-width: 640px) 50vw, 100vw"
  width="1600"
  height="900"
  alt="Team standing in front of the new office"
/>

Read sizes as a list of rules, first match wins:

  • viewport 1024px and up: the image is 360px wide
  • viewport 640px and up: half the viewport
  • otherwise: full viewport width

The w values in srcset describe the real pixel width of each file. The browser combines the two, multiplies by the device pixel ratio, and picks the smallest file that is still big enough.

sizes does not have to be perfect. It has to be close. Being 10% off costs you nothing. Leaving it out costs you the whole optimisation.

Always set width and height

<img ... width="1600" height="900" />

These are not the display size. CSS still controls that. They give the browser the aspect ratio so it can reserve the right space before the file arrives, which stops the page from jumping. That jump is measured by Cumulative Layout Shift and it is the easiest Core Web Vitals point you will ever score.

Pair it with:

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

Format switching with picture

Use <picture> when you need different files rather than different sizes, for example modern formats with a fallback:

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

The browser takes the first <source> it understands. AVIF is usually 30 to 50% smaller than WebP for photos, and WebP is smaller than JPEG. Order matters: put the best format first.

Art direction is a different job

If the crop should change, not just the resolution, that is also <picture>:

<picture>
  <source media="(max-width: 640px)" srcset="hero-square.jpg" />
  <img src="hero-wide.jpg" width="1600" height="900" alt="..." />
</picture>

A wide hero often loses its subject on a phone. This lets you ship a squarer crop for small screens.

Loading and priority

  • Above the fold hero: fetchpriority="high" and no lazy loading.
  • Everything below the fold: loading="lazy".
  • Add decoding="async" to keep decoding off the main thread.
<img src="hero.jpg" fetchpriority="high" decoding="async" ... />
<img src="thumb.jpg" loading="lazy" decoding="async" ... />

Do not set loading="lazy" on the hero. It delays your Largest Contentful Paint, which is exactly the image you want early.

Alt text, quickly

  • Meaningful image: describe what matters, not "image of".
  • Decorative image: alt="", empty and present. Leaving alt out entirely makes some screen readers read the file name aloud.
  • Image inside a link with no other text: the alt text becomes the link name, so write it as a destination, for example "Read the full case study".

Written by Rahul Mourya, full stack engineer. Spotted a mistake or want the follow-up post? Email me.

All posts
  • HTML2 min read

    One embedded video cost me two seconds of load time

    A YouTube embed pulls in about a megabyte before anyone presses play. Here is the lazy facade pattern, plus the sandbox and allow attributes that keep third party frames contained.

    • iframe
    • performance
    • security
  • HTML2 min read

    The viewport meta tag that breaks pinch to zoom

    user-scalable=no and maximum-scale=1 are copied into projects every day. They fail an accessibility audit, and the bug they were added to fix has another cause.

    • viewport
    • mobile
    • accessibility
  • HTML3 min read

    The autocomplete tokens that fix mobile checkout

    autocomplete is not on or off. It is a list of specific tokens that let phones fill an address in one tap. Getting them right is the cheapest conversion win in a form.

    • forms
    • mobile
    • autofill