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
- Put a 2400px image on a page, view it at 360px, and read the transferred size in the Network tab.
- Generate 400/800/1200/2000 versions and add
srcsetwithsizes="100vw". Reload at 360px and compare. - Now display the image in a 300px column but leave
sizesat100vw. Note which file is fetched. Then setsizes="300px". - Set devtools to emulate a 3× device pixel ratio and see which file is chosen.
- Use the
xsyntax for a logo and confirm only one file downloads. - Build a
<picture>with AVIF, WebP and JPEG. Confirm in the Network tab that exactly one is fetched, and which. - Save the same photograph as PNG, JPEG, WebP and AVIF. Compare the four sizes.
- Do art direction: a wide crop above 60rem and a square crop below.
- Add
loading="lazy"to below-the-fold images and confirm they are not fetched until you scroll. - Put
loading="lazy"on the hero, throttle to Slow 4G, and notice the delay. - Remove
widthandheightfrom a large image, throttle, and watch the page jump. Add them back. - Force a portrait photo into a landscape box with no
object-fit, then withcover, then adjustobject-position. - Convert a hero from
background-imageto<img>with a gradient overlay, and addsrcset. Compare the transferred bytes. - Run Lighthouse and read the "Properly size images" and "Serve images in modern formats" items.
Official documentation
- MDN — Responsive images —
srcset,sizesand<picture>explained with the resolution-switching and art-direction cases separated. - MDN — The img element — Every attribute, including
fetchpriorityanddecoding. - MDN — object-fit — All five values, with a visual comparison.
- web.dev — Cumulative Layout Shift — What the metric measures and why image dimensions are the usual fix.
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