Image Optimization

Performance & Core Web Vitals — Image Optimization
Performance & Core Web Vitals
Chapter 5 · Image Optimization

🖼️ Image Optimization

Chapters 2 and 3 both flagged images as the single most common culprit behind poor LCP and CLS. This chapter is the concrete, practical fix — usually the highest-leverage optimization available, since images are typically a page's biggest single contributor to total byte weight.

Modern Formats: WebP and AVIF

WebP is typically 25–35% smaller than an equivalent-quality JPEG; AVIF is often smaller still. Both have broad modern browser support, but a fallback chain via <picture> keeps older browsers working:

Format Fallback With <picture>

<picture> <source srcset="hero.avif" type="image/avif"> <source srcset="hero.webp" type="image/webp"> <img src="hero.jpg" width="1200" height="600" alt="..."> </picture>

The browser tries each <source> in order, falling back to the plain <img> if nothing else is supported — note the width/height on the fallback <img>, still doing Chapter 3's CLS-prevention work regardless of which format actually loads.

Responsive Images: srcset and sizes

A phone shouldn't download a desktop-sized image it'll never display at full resolution — srcset offers multiple resolutions, and sizes tells the browser how much viewport width the image will actually occupy, letting it pick the right candidate before layout even happens:

<img srcset="hero-480.webp 480w, hero-800.webp 800w, hero-1200.webp 1200w" sizes="(max-width: 600px) 100vw, 50vw" src="hero-800.webp" width="800" height="400" alt="...">

🐢 Lazy Loading

Native loading="lazy"

Simplest approach — the browser itself defers offscreen images until they're about to scroll into view, reducing bandwidth competition during initial load with zero JS cost.

IntersectionObserver-Based

More control — custom thresholds, placeholder swap-in animations — but adds JS running on the main thread (Chapter 4's concern), needed mainly for advanced behavior the native attribute doesn't offer.

<img src="below-fold.webp" loading="lazy" width="800" height="400" alt="...">

Image CDNs

An image CDN resizes, converts formats, and compresses on the fly based on URL parameters — often negotiating format automatically via the browser's Accept header (AVIF to browsers that support it, WebP otherwise, JPEG as a final fallback), offloading this entire pipeline from the origin server.

Unoptimized vs Fully Optimized

Single-Size JPEG

One large JPEG served to every device regardless of screen size — wasted bytes on mobile, no format negotiation, no lazy loading.

Responsive AVIF/WebP

Right format, right resolution, right loading strategy for above vs below the fold — meaningfully lighter and faster across every device.

💻 Coding Challenges

Challenge 1: Write a Responsive <picture>

Write a <picture> element for a below-the-fold product image, with AVIF and WebP sources, a JPEG fallback with dimensions, and native lazy loading applied correctly.

Goal: Practice combining format fallback, dimensions, and lazy loading in one correct element.

→ Solution

Challenge 2: Choose Native vs IntersectionObserver

A team needs offscreen images to fade in smoothly with a custom animation as they load, well before native loading="lazy"'s default threshold. Recommend an approach and justify it using this chapter's tradeoffs.

Goal: Practice recognizing when the simpler native option genuinely isn't sufficient.

→ Solution

Challenge 3: Write srcset/sizes for a Two-Column Layout

An image occupies 100% of viewport width on screens under 700px, and 50% of viewport width otherwise. Write the sizes attribute for this layout, and three reasonable srcset candidates.

Goal: Practice writing an accurate sizes value that actually matches a real layout.

→ Solution

⚠️ Gotcha: Lazy-Loading the LCP Image Itself

Applying loading="lazy" to an above-the-fold hero image directly contradicts Chapter 2's guidance — the LCP element needs to be discovered and fetched as early as possible, ideally preloaded. Native lazy loading works by deferring a fetch until the browser judges an image is about to scroll into view; for an image that's already visible on load, this actively delays it, often making LCP significantly worse. Never apply loading="lazy" to the LCP candidate or anything else visible above the fold — reserve it strictly for offscreen, below-the-fold images.

🎯 What's Next

With images covered, the next chapter widens the lens: Lazy Loading Beyond Images — code splitting, dynamic imports, and the same above/below-the-fold tradeoff applied to JavaScript and components.