RizTech Academy logo
RizTech Academy
Responsive DesignLesson 3 of 725 min

Responsive images and modern formats

Sort any real page's Network tab by size and the top rows are images. Not your CSS, not your fonts, not your HTML — images, usually by a factor of ten. So this is the lesson with the largest effect on whether your site is usable on mobile data, and the core of it is one attribute.

The problem

<img src="shop.jpg" alt="The shopfront" width="2400" height="1600">
img { max-width: 100%; height: auto; }

That is correct and responsive — it never overflows. And a phone showing that image 360 CSS pixels wide has still downloaded all 2400 pixels of it, perhaps 800 KB for something displayed at a fifteenth of its data. On a metered connection that is real money for nothing.

srcset: let the browser choose

<img
  src="shop-800.jpg"
  srcset="shop-400.jpg   400w,
          shop-800.jpg   800w,
          shop-1200.jpg 1200w,
          shop-2000.jpg 2000w"
  sizes="(width >= 60rem) 50vw, 100vw"
  alt="The shopfront of Sharma Kirana on Karve Road"
  width="1200" height="800">

Four things to understand, and the third is the one people get wrong.

srcset lists the files with their actual pixel widths. The w is the real width of the file, not a condition. You are describing your files, not making a decision.

src is the fallback for anything that does not understand srcset. Put a middling size there.

sizes tells the browser how wide the image will be displayed — and this is the part that is genuinely hard, because the browser needs it before it has read your CSS. The image is discovered and requested during HTML parsing, before layout, so it cannot measure anything. You are promising it how big the image will end up.

Read sizes as a list of conditions with a resulting width, first match wins: (width >= 60rem) 50vw, 100vw means "on wide screens it is half the viewport, otherwise the full viewport".

Get sizes wrong and everything downstream is wrong. The default is 100vw, so an image displayed in a 300px sidebar on a 1400px screen will download a 1400px file unless you say sizes="300px". This is the single commonest cause of a page being mysteriously heavy despite having srcset.

The browser makes the final choice, and it accounts for device pixel ratio. A phone at 360 CSS pixels with a 3× screen wants around 1080 real pixels, so it picks shop-1200.jpg. You never compute that yourself. Some browsers also take the connection into account.

The simpler x syntax

When an image is always the same displayed size — a logo, an avatar, an icon:

<img src="logo.png" srcset="logo.png 1x, logo@2x.png 2x, logo@3x.png 3x"
     alt="Sharma Kirana" width="160" height="40">

No sizes needed, because the display width is fixed. Use w when the image is fluid, x when it is not.

<picture>: when you must choose

srcset lets the browser pick a size. <picture> lets you dictate the file, and there are two reasons to.

Different formats:

<picture>
  <source srcset="shop.avif" type="image/avif">
  <source srcset="shop.webp" type="image/webp">
  <img src="shop.jpg" alt="The shopfront" width="1200" height="800">
</picture>

The browser takes the first <source> whose type it supports. AVIF first because it is smallest, then WebP, then JPEG as the universal fallback. The <img> is required — it is what actually renders, and it carries the alt, width and height.

Art direction — a genuinely different crop, not just a smaller one:

<picture>
  <source media="(width >= 60rem)" srcset="shop-wide.jpg">
  <img src="shop-square.jpg" alt="The shopfront" width="800" height="800">
</picture>

A wide landscape hero loses its subject when squeezed into a narrow phone screen. Here you ship a squarer crop to phones. This is the test: if a smaller version of the same crop would do, use srcset. If you need a different crop, use <picture>.

You can combine them — srcset inside each <source> — but do not reach for that until you need it.

Which format, and what it costs

Format Use for Rough size vs JPEG
AVIF photographs 50% smaller
WebP photographs 30% smaller
JPEG photographs, universal fallback baseline
PNG screenshots, real transparency often much larger
SVG logos, icons, anything drawn usually tiny

AVIF is supported in every current browser. It encodes slowly, which matters to your build and not to your visitors.

Two mistakes that cost the most:

A PNG of a photograph. It can be five times the JPEG for no visible benefit. PNG is for flat colour and transparency.

A raster logo. A logo as SVG is usually 2–5 KB, sharp at any size, and can inherit currentColor. As a PNG it needs three files and still looks soft somewhere.

loading and fetchpriority

<!-- the hero: the visitor is waiting for this one -->
<img src="hero.jpg" alt="…" width="1200" height="600" fetchpriority="high">

<!-- everything below the fold -->
<img src="product.jpg" alt="…" width="600" height="400" loading="lazy">

loading="lazy" on everything below the fold, and never on the hero — you would be delaying the one image the visitor came for. fetchpriority="high" on the hero tells the browser to fetch it ahead of other images.

decoding="async" is occasionally useful for a long gallery; it lets the browser decode off the main thread so scrolling stays smooth.

Always width and height

<img src="shop.jpg" alt="…" width="1200" height="800">

Repeating this from module 1 because it is the highest-value two attributes on the page. They are the intrinsic dimensions; CSS still controls the display size. Giving them lets the browser reserve the right space before the file arrives, so nothing jumps.

That jump is Cumulative Layout Shift, and it is why you sometimes tap a link and hit the wrong thing. Lighthouse measures it and images without dimensions are the usual cause.

aspect-ratio in CSS does the same job when the dimensions are not known in the HTML:

.card img { aspect-ratio: 3 / 2; object-fit: cover; }

object-fit, and the squashing default

img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;        /* fill the box, crop the overflow */
  object-position: center;
}

Without object-fit, forcing a portrait photo into a landscape box stretches it — the default is fill, which distorts. cover crops instead, which is almost always what you want. contain fits the whole image and leaves gaps.

object-position: 50% 30% is the fix when cover keeps cropping somebody's head off.

How many files to actually make

Not eight. Three or four widths per image is plenty:

400, 800, 1200, 2000

Each in AVIF and WebP with a JPEG fallback is twelve files per image, which is why this is a build-tool job in real projects. For this course, generate them by hand for the capstone's few images — it is worth doing once so you know what the tooling is doing for you.

And resize the original first. A 4000-pixel phone photograph dropped in as-is is the single most common cause of a slow page. Roughly twice the largest displayed size is enough; beyond that the file grows and nobody can see a difference.

A background image cannot do any of this

Repeating the module 1 rule with the performance reason attached: a CSS background-image has no srcset, no loading="lazy", and no fetchpriority. It is also discovered later, after the stylesheet is parsed.

So a hero photograph belongs in an <img>, with a gradient overlay for the text, not in a background-image. Backgrounds are for textures and gradients.

Check your work

Why max-width: 100% is not enough. It scales the image; it does not stop the download.

What the w in srcset is. The file's real pixel width — you are describing your files.

Why sizes is needed at all. The image is requested during HTML parsing, before layout, so the browser cannot measure it.

The commonest srcset mistake. Leaving sizes at its 100vw default for an image displayed much narrower.

Who makes the final choice. The browser, accounting for device pixel ratio.

w versus x. Fluid image versus fixed-size image.

srcset versus <picture>. A smaller version of the same crop, versus a genuinely different crop or format.

Why <picture> still needs an <img>. It is what renders and carries the alt, width and height.

Where never to put loading="lazy". The hero.

What width and height prevent. Layout shift.

What happens without object-fit. The image is stretched — the default is fill.

Why a hero belongs in an <img>. A background image has no srcset, no lazy loading and no priority hint.

Practice

  1. Put a 2400px image on a page, view it at 360px, and read the transferred size in the Network tab.
  2. Generate 400/800/1200/2000 versions and add srcset with sizes="100vw". Reload at 360px and compare.
  3. Now display the image in a 300px column but leave sizes at 100vw. Note which file is fetched. Then set sizes="300px".
  4. Set devtools to emulate a 3× device pixel ratio and see which file is chosen.
  5. Use the x syntax for a logo and confirm only one file downloads.
  6. Build a <picture> with AVIF, WebP and JPEG. Confirm in the Network tab that exactly one is fetched, and which.
  7. Save the same photograph as PNG, JPEG, WebP and AVIF. Compare the four sizes.
  8. Do art direction: a wide crop above 60rem and a square crop below.
  9. Add loading="lazy" to below-the-fold images and confirm they are not fetched until you scroll.
  10. Put loading="lazy" on the hero, throttle to Slow 4G, and notice the delay.
  11. Remove width and height from a large image, throttle, and watch the page jump. Add them back.
  12. Force a portrait photo into a landscape box with no object-fit, then with cover, then adjust object-position.
  13. Convert a hero from background-image to <img> with a gradient overlay, and add srcset. Compare the transferred bytes.
  14. Run Lighthouse and read the "Properly size images" and "Serve images in modern formats" items.

Official documentation

Next: type that scales without a media query.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship