Freeframe

How to Use Responsive Images with srcset and sizes

Export each photo in a few widths, list them in srcset, add sizes="auto" to lazy-loaded images and load the hero first, so phones never download desktop photos.

· 7 min read

To stop phones downloading desktop-sized photos, export each photo in three or four widths, list them in the image's srcset attribute, and tell the browser how wide the image is shown with sizes. For images below the fold, add loading="lazy" and start sizes with auto, so the browser measures the layout and picks the right file itself. Keep the main image at the top of the page eager, and give every image width and height. It takes about ten minutes per page, and it is usually the biggest speed win on a small site with real photos.

Why now#

sizes="auto" used to work only in Chrome, Edge and Firefox. Safari 27, released in September 2026, added it, which makes it Baseline Newly available: supported in every major browser engine (web.dev, New to the web platform in September). Before that, you had to work out a sizes value by hand for every image, and most people either got it wrong or left it out. Now the fiddly part is optional for lazy-loaded images, which are most of the photos on a typical page.

It also matters because the end of the year is when many small businesses swap in new photos: a Christmas menu, gift ideas, a new product range. Each one is a chance to upload a 4,000-pixel phone photo by accident.

How the browser chooses an image#

An <img> with only src gives the browser one file, whatever the screen. With srcset and width descriptors, you give it a list and let it choose:

html
<img
  src="assets/img/dining-room-800.webp"
  srcset="
    assets/img/dining-room-400.webp   400w,
    assets/img/dining-room-800.webp   800w,
    assets/img/dining-room-1600.webp 1600w
  "
  sizes="(min-width: 1024px) 50vw, 100vw"
  width="1600"
  height="1067"
  alt="The dining room set for dinner, with candles on every table"
/>

The 400w after each file is its real width in pixels, and MDN says it "must match the intrinsic width of the referenced image" (MDN: the img element). sizes tells the browser how wide the image will be displayed: here, half the window on large screens and the full width otherwise. The browser multiplies that by the screen's pixel density and downloads the smallest file that is big enough. A phone 400 pixels wide at 2× density gets the 800-pixel file; a laptop showing the image in a 640-pixel column at 1× gets the 800-pixel file too, not the 1600.

If you leave sizes out, the browser assumes 100vw, the full width of the window (MDN). For an image in a three-column grid that means downloading a file about three times wider than it needs to be.

Step 1: Export each photo in a few widths#

Decide the largest size the image is ever shown at, double it for high-density screens, and export that plus two or three smaller versions. For most small-business photos, 400, 800 and 1600 pixels wide cover phones, tablets and desktops. A full-width hero might add 2400.

Save the copies as WebP, which every current browser displays. Most image editors have an "export for web" option. If you have ImageMagick installed, one command per width does it:

sh
magick dining-room.jpg -resize "800x>" -quality 80 dining-room-800.webp

800x> sets the width to 800 pixels and keeps the aspect ratio; the > means "shrink image only if dimensions are larger than specified bounds", so a small original is never blown up. On older ImageMagick 6 installs the command is convert instead of magick.

Name the files with their width (-400, -800, -1600) so the srcset is easy to read and check.

Step 2: Lazy-load images below the fold with sizes="auto"#

For any image that isn't visible when the page first opens (gallery photos, team pictures, product cards), this is all you need:

html
<img
  src="assets/img/tasting-menu-800.webp"
  srcset="
    assets/img/tasting-menu-400.webp   400w,
    assets/img/tasting-menu-800.webp   800w,
    assets/img/tasting-menu-1600.webp 1600w
  "
  sizes="auto, (min-width: 1024px) 33vw, 100vw"
  loading="lazy"
  width="1600"
  height="1067"
  class="aspect-[3/2] w-full rounded-2xl object-cover"
  alt="Five small plates from the autumn tasting menu"
/>

loading="lazy" delays the download until the image is close to the screen. By then the browser has laid out the page, so auto lets it "use the expected layout width of the element to select the image to display", and it is "only valid when combined with loading="lazy"" (MDN). You don't have to know your grid's breakpoints.

The values after auto are a fallback for older browsers that don't understand it; they ignore the auto and use the rest of the list. If you can't be bothered to work them out, auto, 100vw is no worse than leaving sizes out altogether.

Set width and height to the size of the largest file in the srcset. MDN recommends this so the browser can reserve space with the right aspect ratio, and points out that lazy-loaded images without dimensions start at 0×0 and may never be loaded. CSS such as w-full still controls the size on screen; the attributes only supply the shape.

Step 3: Load the main image first#

The opposite applies to the large image at the top of the page, usually the hero. It is often the page's Largest Contentful Paint (LCP) element, the measure Google uses for loading speed, and web.dev's advice is direct: "Don't lazy-load images that are likely to be in-viewport when the page loads, especially LCP images" (web.dev: Browser-level image lazy loading).

So the hero keeps the default eager loading, gets a hand-written sizes, and can ask for a head start with fetchpriority="high":

html
<img
  src="assets/img/hero-1600.webp"
  srcset="
    assets/img/hero-800.webp   800w,
    assets/img/hero-1600.webp 1600w,
    assets/img/hero-2400.webp 2400w
  "
  sizes="100vw"
  fetchpriority="high"
  width="2400"
  height="1350"
  alt="The shop front on a winter evening, lights on in the window"
/>

Don't add auto here: without loading="lazy" it doesn't apply. Use fetchpriority="high" on one image per page at most; it only means something relative to the other images (MDN).

In Next.js, Astro and WordPress#

Next.js. The next/image component builds srcset for you when Next.js optimizes images. Its docs say to pass sizes when the image uses fill or is made responsive with CSS, and that without it "the browser assumes the image will be as wide as the viewport". Since Next.js 16, the old priority prop is deprecated in favor of preload, though the docs suggest loading="eager" or fetchPriority="high" in most cases (Next.js Image component). A site built as a static export with images: { unoptimized: true }, as the Freeframe Next.js templates are, serves the source file as-is, so write the srcset yourself on a plain <img>, exactly as above.

Astro. Put photos in src/ rather than public/, so Astro can process them, and use <Image /> from astro:assets. Give it layout="constrained" or layout="full-width" and it generates srcset and sizes; priority sets loading="eager", decoding="sync" and fetchpriority="high" for the hero (Astro image reference). Files in public/ are copied untouched, so for those, use the plain HTML above.

WordPress. WordPress creates several sizes of every image you upload and writes the srcset itself. Since WordPress 6.7 it also adds auto to sizes on lazy-loaded images (Make WordPress Core), so the main job is uploading a good original rather than an enormous one. Our guide to updating WordPress safely covers getting onto a current version.

Check it worked#

  1. Open the page in Chrome or Edge, open DevTools, and select the Network tab with the Img filter.
  2. Reload and look at the file names: on a narrow window you should see the -400 or -800 versions.
  3. Turn on device emulation, pick a phone, reload, and check again. Hover over an <img> in the Elements tab to see its rendered size next to the file's real size.
  4. Scroll down: lazy images should appear in the Network list as they come near the screen, not on first load.
  5. Run the page through PageSpeed Insights and read its image and LCP suggestions. Oversized images or a lazy-loaded LCP image will be flagged there.

Common mistakes#

  • Width descriptors that don't match the file. If hero-1600.webp is really 1200 pixels wide, the browser picks the wrong file. Check the actual size after exporting.
  • Mixing w and x descriptors in one srcset. MDN calls this incorrect; use one kind.
  • Lazy-loading everything. Fine for the gallery, bad for the hero. Keep the top image eager.
  • Forgetting width and height. The page jumps as images arrive, and lazy images can fail to load at all.
  • Missing alt text. Describe what the photo shows; use alt="" only for purely decorative images. See our accessibility notes.

Next steps#

Freeframe templates ship with SVG artwork, which is already sharp at any size. When you replace it with your own photos, keep the same aspect ratio, as described in Customizing a template, and use the patterns above. Image-led designs such as Bistro, Folio and Market are good places to start. Before launch, run through the website launch checklist, which includes a speed check.

Templates for this

Keep reading