Image & Media Optimization

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.

1Why images are the #1 lever here

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]

  1. Right format — AVIF / WebP with a JPEG fallback. Same picture, far fewer bytes.
  2. Right sizesrcset/sizes so a phone never downloads a 2000px desktop image.
  3. Right loading — lazy-load below the fold, but prioritize the LCP image (never lazy-load it).
  4. Reserved spacewidth/height so the image can't shift layout (CLS).
One-liner

“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.”

2Right format — AVIF & WebP, with a fallback

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]

Trade-off — format isn't free, and “smaller” has limits. AVIF encodes slower and at very high quality the WebP/AVIF gap narrows; for tiny icons or sharp line-art, vector SVG beats all of them and PNG keeps lossless transparency. Don't dogmatically AVIF-everything — match format to content, and let your pipeline (§6) decide per-image. The decode cost is also real on low-end devices, though usually dwarfed by the bytes saved.
One-liner

“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.”

3Right size — srcset & sizes

The 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:

The browser's math: needed px = slot width (from sizes) × device pixel ratio → pick the smallest file ≥ that. Same markup, four devices:

Deviceviewportsizes → slot× DPRneedspicks
Old phone390px≤600 → 100vw = 390×2780800w
iPhone Pro390px≤600 → 100vw = 390×311701600w
Laptop (grid)1200pxelse → 33vw = 396×1396400w
Retina laptop1440pxelse → 33vw = 475×29501600w

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)

resolution switching — same image, many sizes

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> + media

art direction — different crop per breakpoint

When 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 decidesWhat changes
srcset + sizesthe browserjust the size/resolution of the same image (resolution switching)
<picture> + mediayou, the authora 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.)

Trap — wrong 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>.
One-liner

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.”

Where does 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)✅ enoughjust <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>
The scale twist — an image CDN often deletes the format axis. That verbose <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.
One-liner

“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.”

4Right loading — lazy below, prioritize the LCP image

This is the most-tested image trap in the round. Two opposite moves for two kinds of image:

ImageDoWhy
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 lazyIt'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="...">
Trap — lazy-loading the LCP image is a classic self-own. A blanket “lazy-load all images” rule (or a CMS default) catches the hero too, so the browser deprioritizes the very image LCP measures → LCP gets worse. Rule: eager + high priority above the fold, lazy below. For an even earlier start, preload the hero with fetchpriority="high" (Lesson 01) so it's discovered before the parser reaches it.
One-liner

“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.”

Network-aware (adaptive) loading

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:

MechanismWhereUse / caveat
Save-Data: on headerserver / CDNBrowser 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 API
navigator.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)CSSSkip/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
}
Trade-off — lead with intent, not guessing. 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-hooksuseSaveData/useNetworkStatus.)
One-liner

“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.”

5Reserved space — kill image CLS

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:

One-liner

“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.”

6At scale — don't hand-author, build a pipeline

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:

Trade-off — a CDN/component is a dependency & a cost. An image CDN is a per-image transform + bandwidth bill and a third party in your critical path (resilience: cache + a fallback origin). A framework component locks images to that framework's conventions. Worth it at the platform scale — the alternative is every team hand-rolling <picture> inconsistently — but name the cost, don't pretend it's free.
Full loop

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.”

7Check yourself — scenario quiz

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

Try this aloud before next session: “Our hotel results page ships ~2 MB of images and LCP is poor on mobile. Walk me through how you'd fix it — format, size, loading, layout — and how you'd keep it optimal as 20 teams add new pages.” Time to 90 seconds.
Good follow-up topics:
“Quiz me out loud, harder” “DPR / x-descriptors vs w-descriptors?” “How does an image CDN do content negotiation?” “LQIP / blur placeholder — how?” “Video/hero-video optimization?” “content-visibility for long image lists?”