For a hotel site, images are the page — and the biggest lever on LCP, bytes, and CLS. The four disciplines: right format, right size, right loading priority, reserved space — then automate it at scale.
On a travel/booking site the hero shot and the result-card thumbnails are the product. That means images are usually both the LCP element and the largest transfer — so optimizing them moves the metrics you're graded on more than almost anything else. Four overlapping disciplines, and you want all four: [MDN]
srcset/sizes so a phone never downloads a 2000px desktop image.width/height so the image can't shift layout (CLS).“On a hotel site the images are the page — they're the LCP element and the heaviest bytes. So image strategy isn't a detail, it's the main performance lever: right format, right size, right priority, reserved space.”
Modern formats encode the same quality in dramatically fewer bytes: WebP ≈ 25–35% smaller than JPEG, AVIF ≈ 50% smaller. [web.dev] The <picture> element lets the browser pick the first format it supports — AVIF, then WebP, then a JPEG that always works:
<picture>
<source type="image/avif" srcset="hotel.avif">
<source type="image/webp" srcset="hotel.webp">
<img src="hotel.jpg" alt="Pool at sunset" width="800" height="600">
</picture>
The browser stops at the first <source> whose type it supports; the <img> is the mandatory fallback (and carries the alt, width/height, loading attrs). [MDN]
“AVIF first, WebP fallback, JPEG as the floor — via <picture> so the browser self-selects. Roughly half the bytes of JPEG for the same shot, which on a thumbnail-heavy results page is enormous.”
srcset & sizesThe other half of the win: don't ship a 2000px image to a 400px phone. srcset lists the same image at multiple widths; sizes tells the browser how wide the image will render so it can pick the smallest sufficient file before layout (the preload scanner needs sizes — Lesson 02). [web.dev]
<img
srcset="hotel-400.jpg 400w, hotel-800.jpg 800w, hotel-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 33vw"
src="hotel-800.jpg" alt="..." width="800" height="600">
Read the two attributes as inventory vs slot:
srcset = the menu of files I have. hotel-800.jpg 800w means “this file is literally 800px wide.” The w is a width descriptor — just declaring what's on the shelf, all the same photo at different sizes.sizes = how wide the image will render, read like CSS (first match wins): “≤600px viewport → 100vw (full width); otherwise → 33vw (a 3-col grid).” You must tell the browser this because it picks the file before layout/CSS (the preload scanner — Lesson 02), so it can't otherwise know the slot is a third of the screen.src is only the fallback for ancient browsers; modern ones use the menu.The browser's math: needed px = slot width (from sizes) × device pixel ratio → pick the smallest file ≥ that. Same markup, four devices:
| Device | viewport | sizes → slot | × DPR | needs | picks |
|---|---|---|---|---|---|
| Old phone | 390px | ≤600 → 100vw = 390 | ×2 | 780 | 800w |
| iPhone Pro | 390px | ≤600 → 100vw = 390 | ×3 | 1170 | 1600w |
| Laptop (grid) | 1200px | else → 33vw = 396 | ×1 | 396 | 400w |
| Retina laptop | 1440px | else → 33vw = 475 | ×2 | 950 | 1600w |
Note the laptop pulls the tiny 400w file — in a 3-column grid the slot is only ~400px even on a big screen — while the phone pulls 1600w because its DPR is 3. That's the whole point: resolution switching serves each device the smallest sufficient file. [MDN]
Use srcset/sizes for the same photo at different sizes. When you instead want a different crop per breakpoint, that's a job for <picture>:
srcset + sizes (w-descriptors)The common case: one photo at several widths, browser picks by viewport + DPR. This is what kills the “2 MB image on a phone” problem on a results grid.
<picture> + mediaWhen you want a different image, not just a smaller one — e.g. a tight square crop on mobile vs a wide hero on desktop. Use <source media="...">.
Scaling one wide pool shot down to a phone makes the subject a tiny strip — so on mobile you'd rather show a different, tighter crop. The browser can't make that editorial call by scaling; you declare it with <source media> (browser uses the first matching media):
<picture>
<source media="(max-width: 600px)" srcset="hotel-square.jpg"> // phone: tight crop
<img src="hotel-wide.jpg" alt="..." width="1200" height="600"> // desktop default
</picture>
| Who decides | What changes | |
|---|---|---|
srcset + sizes | the browser | just the size/resolution of the same image (resolution switching) |
<picture> + media | you, the author | a genuinely different image / crop per breakpoint (art direction) |
(And <picture> + <source type> from §2 switches format — so <picture> handles format and art-direction; srcset/sizes handles resolution. You often combine them.)
sizes silently wastes the whole effort. If sizes doesn't match the real rendered width (e.g. you say 100vw but it renders at 33vw in a grid), the browser picks a needlessly large file and you've shipped the bytes anyway. sizes is the part teams get wrong — verify it against the actual layout. And srcset only does resolution switching; for a different crop you need <picture>.
“srcset is ‘same photo, pick the right size for me’ — the browser chooses by viewport × DPR. <picture media> is ‘show a different photo here’ — my editorial call. Srcset for performance; picture-media only when the crop must change.”
srcset go — and do I even need <picture>?srcset works on both <img> and <source>. That's the whole answer to “why <picture>?”: a plain <img srcset sizes> already does resolution switching, so prefer it — and only reach for <picture> for the two things srcset structurally can't do:
| You need… | Plain <img srcset> | Why / solution |
|---|---|---|
| Same image, different sizes (resolution) | ✅ enough | just <img srcset sizes> — don't add <picture> |
| Different formats (AVIF→WebP→JPEG) | ❌ | srcset picks by size, not by browser support → needs <picture>+<source type> |
| Different crop per breakpoint (art direction) | ❌ | srcset only resizes the same image → needs <picture>+<source media> |
In a <picture>, the browser reads <source>s top-to-bottom and uses the first whose type AND media both match, then applies that source's srcset/sizes; the <img> is the required fallback. Attributes split by role: srcset/sizes/type/media live on <source>; alt/width/height/loading/fetchpriority/decoding live only on the <img>. The kitchen-sink — all three axes at once: [MDN]
<picture>
// AVIF, mobile crop (media) at 2 sizes (srcset/sizes)
<source type="image/avif" media="(max-width:600px)"
srcset="sq-400.avif 400w, sq-800.avif 800w" sizes="100vw">
// AVIF, desktop wide crop
<source type="image/avif"
srcset="wide-800.avif 800w, wide-1600.avif 1600w" sizes="66vw">
// WebP fallback (same two crops) ...
<source type="image/webp" media="(max-width:600px)" srcset="sq-800.webp 800w">
<source type="image/webp" srcset="wide-1600.webp 1600w">
// JPEG floor — REQUIRED; carries alt/dims/loading/priority
<img src="wide-800.jpg"
srcset="wide-800.jpg 800w, wide-1600.jpg 1600w" sizes="66vw"
alt="Pool at sunset" width="1600" height="900"
fetchpriority="high" decoding="async">
</picture>
<picture> is rarely hand-written at scale. An image CDN (§6) does format negotiation on the server via the request's Accept header — the browser says “I accept AVIF,” the CDN returns AVIF from a single <img src> URL. So the format <source>s vanish and you're back to a plain <img srcset sizes>, reaching for <picture media> only for genuine art-direction crops.
“A plain <img srcset> covers resolution — that's most images. I only add <picture> for format fallback or art direction, the two things srcset can't express. And with a CDN negotiating format off the Accept header, even the format case disappears.”
This is the most-tested image trap in the round. Two opposite moves for two kinds of image:
| Image | Do | Why |
|---|---|---|
| Below-the-fold (cards down the list, footer) | loading="lazy" | Don't download until near the viewport → saves bytes & bandwidth, frees the network for what's visible. |
| The LCP image (hero / first result) | fetchpriority="high", never lazy | It's the metric. Raise its priority so it downloads first; Google's own test cut LCP 2.6s → 1.9s with that one attribute. [web.dev] |
// hero — the LCP element: prioritize, don't lazy-load
<img src="hero.avif" fetchpriority="high" width="1200" height="600" alt="...">
// thumbnails further down: lazy + async decode
<img src="card.avif" loading="lazy" decoding="async" width="300" height="200" alt="...">
preload the hero with fetchpriority="high" (Lesson 01) so it's discovered before the parser reaches it.
“Lazy-load below the fold, but the LCP image gets fetchpriority=\"high\" and is never lazy. The most common own-goal I see is a blanket lazy rule that catches the hero and tanks LCP.”
A user on 2G in a data-capped market shouldn't get the same 1600px AVIF as someone on office fibre. Adaptive serving tunes quality/resolution — or whether to load an image at all — to the connection. Three mechanisms, most-robust first:
| Mechanism | Where | Use / caveat |
|---|---|---|
Save-Data: on header | server / CDN | Browser sends it when Data Saver is on → CDN returns a lighter image from the same URL. Works without JS, honors an explicit user choice — the preferred path. Must send Vary: Save-Data or the cache crosses the wires. |
Network Information APInavigator.connection | JS (client) | saveData, effectiveType (slow-2g…4g), downlink, rtt → pick a smaller srcset, skip the hero video, drop decorative images. Chromium-only (no Safari/desktop Firefox) & just an estimate → enhancement, never a gate. [MDN] |
@media (prefers-reduced-data) | CSS | Skip/swap CSS background images when Data Saver is on. Cleanest for decorative images; limited support → enhancement. |
// JS: progressive enhancement on top of a sensible default
const c = navigator.connection;
if (c && (c.saveData || c.effectiveType === 'slow-2g' || c.effectiveType === '2g')) {
img.src = lowResUrl; // lighter image / skip hero video
}
effectiveType is an estimate from recent throughput (can be wrong) and a fingerprinting surface, and the JS API is Chromium-only — so it can't be your only strategy. Prefer the Save-Data header at the CDN: it's an explicit user signal and works server-side everywhere. Layer the JS API on top for browsers that expose it, and always degrade to a sane default. (React: Google's react-adaptive-hooks — useSaveData/useNetworkStatus.)
“For data-sensitive markets I serve a lighter image set to Save-Data users at the CDN — explicit intent, works everywhere. navigator.connection is Chromium-only and just a guess, so it's enhancement on top, never a gate — and I Vary: Save-Data so the cache doesn't cross the wires.”
An image with no dimensions loads, takes up space that wasn't reserved, and shoves the content below it down — that's layout shift (CLS, Lesson 03). The fix is to always let the browser reserve the box before the pixels arrive:
width and height (the intrinsic ratio). Modern browsers turn those attributes into an aspect-ratio and reserve the space — even while the image is still loading. [web.dev]aspect-ratio on the element so the box is reserved regardless.img{height:auto} with width/height attributes present keeps the ratio correct across breakpoints.“Every image ships with width and height so the browser reserves the box before the bytes land — that's most of CLS gone. Dimensionless images shoving the page down is the #1 cause of image CLS.”
The Lead answer isn't a perfectly hand-tuned <picture> — nobody maintains that across thousands of hotel photos and many teams. You make it automatic:
<Image> component — Next.js next/image (and equivalents) bake in srcset generation, lazy-loading, blur placeholders, and required dimensions, so every dev gets it right by default. [Next.js]<img>, so it can't regress across teams.<picture> inconsistently — but name the cost, don't pretend it's free.
Concept: right format + size + priority + reserved space, automated. Trade-off: an image CDN/component adds cost & a dependency, so I weigh it against the consistency win at scale. Anchor: “Our results grid shipped 2 MB of oversized JPEGs per page; we moved to an image CDN with AVIF + srcset + fetchpriority on the first row — LCP and total bytes dropped hard.” Impact: images are LCP on ~80% of pages, so this moves the whole product's CWV, not one screen. Invite: “If we were small I'd hand-author <picture>; at our scale the only sane answer is a pipeline + a CI budget so it can't regress.”
Pick an answer; instant feedback. Push-back style, like the round.
1. On a hotel-search results page, why are images the first place you'd look for a performance win?
2. What does <picture> with multiple <source type> give you that a plain <img> doesn't?
3. What is sizes for, and why is it easy to get wrong?
4. A CMS applies loading="lazy" to every image “for performance.” The hero is the LCP element. What happens, and the fix?
scn: LCP got worse after the “optimization.”
5. Your results grid jumps around as thumbnails load. Most direct fix?
6. You only have one hotel.jpg per listing but need a tight square crop on mobile and a wide shot on desktop. Which tool?
7. Most Lead answer to “how do you keep images optimal across a huge, multi-team travel site?”
8. When is AVIF not the obvious choice?
9. With srcset="a-400 400w, a-800 800w, a-1600 1600w" and sizes="33vw", what does a phone at viewport 400px, DPR 3, download?
scn: prove you can do the browser's math.
10. You only need the same hotel photo at different sizes — no format fallback, no crop change. Reach for <picture>?
11. You want data-saver users in low-bandwidth markets to get lighter images. What's the most robust approach?
scn: a teammate proposes navigator.connection.effectiveType in JS as the whole solution.
0 / 11 answered