← All posts

Image Optimization Beyond 'Compress It': AVIF, WebP, and Responsive Delivery

VisibilityIQ

The site has a 3.4-second LCP, the hero is a JPEG, and the ticket says “compress the images.” Someone runs every asset through a compressor, the hero drops from 800KB to 520KB, and LCP improves by maybe 200ms. The real defect was untouched: that hero is a 2400-pixel-wide image being downloaded in full by a phone with a 390-pixel-wide screen. The phone needed an 80KB image and got a 520KB one. Compression optimized the wrong axis.

“Compress it” treats image weight as a quality-setting problem when it’s overwhelmingly a delivery problem. The biggest wins in image optimization come not from squeezing a given file smaller but from sending a different, correctly-sized file to each device — and from choosing a format whose compression is fundamentally better than JPEG’s. Format selection and responsive delivery routinely cut image bytes by 70–90%; a compression pass on a single oversized file gets you 20%. The order of operations is wrong on most sites.

Format Selection: AVIF First, WebP Second, JPEG Last

The format you serve sets the floor for how small the file can be at a given visual quality, and JPEG’s floor is high. JPEG is a thirty-year-old codec; the modern formats beat it decisively.

AVIF, based on the AV1 video codec’s intra-frame compression, produces files roughly 50% smaller than JPEG at equivalent perceived quality, and the advantage widens on photographic content — smooth skies, gradients, skin tones — where JPEG’s block artifacts force you to raise quality (and bytes) to hide them. AVIF also supports a wider color gamut and proper alpha transparency, so it replaces PNG for graphics with transparency too. Its costs are real but bounded: encoding is CPU-heavier than JPEG, and a thin tail of older clients can’t decode it.

WebP is the pragmatic middle: roughly 25–35% smaller than JPEG, near-universal browser support, fast to encode. It’s the fallback that covers almost everything AVIF doesn’t.

You don’t choose between them — you offer all three and let the browser pick:

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

The browser evaluates the <source> elements top to bottom and uses the first type it can decode, falling through to the <img> as the universal backstop. No JavaScript, no user-agent detection, no server-side content negotiation required. The clients that support AVIF get AVIF’s savings; everyone else degrades cleanly. This is the format strategy in one block of markup.

Responsive Delivery: The srcset and sizes Mechanism

Format fixes the per-pixel cost. Responsive delivery fixes the far larger problem of pixel count — sending a desktop-resolution image to a phone. This is where srcset and sizes earn their keep, and where most of the wasted bytes on the web live.

srcset declares a set of image candidates at different widths, annotated with their intrinsic pixel width using the w descriptor:

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

sizes tells the browser how wide the image will be displayed at various breakpoints — and crucially, the browser evaluates sizes before layout, using it together with the device pixel ratio to compute the exact candidate to download. A phone at 390 CSS pixels with a 3× display, rendering the image at 100vw, needs roughly 1170 device pixels and picks the 1200w candidate — not the 2400w one a naive desktop default would have served. The desktop at 1400px wide, rendering at the 1200px slot, picks 1200w. Each device downloads the candidate that matches its actual rendered resolution, and nothing larger.

The combination of format and responsive delivery is multiplicative. A 2400px JPEG hero at 800KB, re-served as an appropriately-sized AVIF candidate to a mobile device, lands around 60–90KB — close to a 90% reduction in transfer for that user, with no perceptible quality loss because the extra pixels were never visible on that screen anyway. The single most common image-optimization failure is shipping one large image to all devices; srcset/sizes is the structural fix.

The one subtlety: sizes must reflect your actual CSS layout. If the image renders at 50% width on desktop but sizes claims 100vw, the browser over-fetches a candidate twice the necessary resolution. A wrong sizes value silently wastes the bytes you were trying to save, so it has to track the layout, ideally generated from the same breakpoint definitions the CSS uses.

Dimensions: The CLS Connection

Every <img> should carry explicit width and height attributes — not for sizing, which CSS handles, but to prevent layout shift. When the browser parses an <img> with no dimensions, it has no idea how tall the element will be until the bytes arrive and decode. It lays out the page as if the image had zero height, then reflows everything below it once the image loads and claims its space. That reflow is Cumulative Layout Shift — content jumping down under the user’s cursor or reading position.

The attributes fix it cleanly. Modern browsers read width and height, compute the intrinsic aspect-ratio, and reserve a correctly-proportioned box before the image loads — even when CSS overrides the actual rendered dimensions (the common img { width: 100%; height: auto; } pattern still honors the attribute-derived aspect ratio). Set the attributes to the image’s real intrinsic dimensions, let CSS control display size, and the layout box is reserved from the first paint. No shift, no CLS contribution from images.

This is a per-image, one-line fix with outsized impact: unsized images are among the most common CLS sources, and the remediation is purely additive — you’re not changing how anything looks, only telling the browser in advance how much room to leave.

Lazy-Loading: Correct Below the Fold, Fatal Above It

loading="lazy" is a genuine win for the long tail of below-the-fold images: the browser defers their fetch until they approach the viewport, so a page with forty images down its length only downloads the handful the user actually scrolls to. Bandwidth saved, main thread unburdened.

Applied to an above-the-fold image, it inverts into a performance bug — and the worst version is lazy-loading the LCP element. The browser, told to lazy-load, won’t fetch the image until it has completed enough layout to determine the element is in (or near) the viewport. For an image that’s already in the viewport at load, this is pure added latency: the fetch that should have started during HTML parse instead waits for layout, injecting 200–800ms of resource load delay directly into LCP. The site lazy-loaded its hero to “save bandwidth” and added three-quarters of a second to its most important metric.

The rule admits no exceptions: nothing in the initial viewport gets loading="lazy". The LCP candidate gets the opposite treatment — loading="eager" (or simply no lazy attribute) plus, for images discovered late by the parser (those inside <picture>, behind srcset, or set as CSS backgrounds), a <link rel="preload" as="image" fetchpriority="high"> with matching imagesrcset/imagesizes so the fetch begins before the parser even reaches the markup. Eager the hero, lazy the rest. The failure mode is almost always a global “lazy-load all images” setting applied without exempting the fold — convenient to switch on, quietly expensive at the top of the page.

CDN Image Transforms: Don’t Pre-Bake the Matrix

Generating four sizes × three formats per image — twelve files for every asset — and committing them to the repo is a maintenance burden that decays the moment a designer adds a breakpoint or swaps a format. The modern alternative is a CDN image-transformation pipeline: store one high-resolution master and let the edge derive the variants on demand from URL parameters.

/hero.jpg?width=800&format=avif&quality=70 returns an 800px AVIF generated and cached at the edge on first request, served from cache thereafter. Cloudflare Images, and the equivalent transform layers on other CDNs, handle format negotiation (often reading the Accept header to serve AVIF or WebP automatically), resizing, quality, and caching without you pre-baking the variant matrix. You author one srcset pointing at parameterized URLs; the CDN materializes exactly the candidates the browsers request and nothing else. This keeps the responsive strategy maintainable as layouts evolve — the breakpoints live in your sizes attribute, not in a directory of pre-rendered files you have to regenerate by hand.

The Practical Sequence

Audit the gap between intrinsic and displayed size first — that’s where the wasted bytes are. A 2400px image rendered at 600px is downloading four times the pixels it shows; fix it with srcset/sizes before you touch quality settings. Then upgrade the format: AVIF primary, WebP fallback, JPEG/PNG backstop, via <picture>. Add explicit width/height to every image to zero out image-driven CLS. Audit lazy attributes and strip them from anything above the fold, eager-loading and preloading the LCP candidate. Push the variant generation to a CDN transform pipeline so the matrix doesn’t ossify.

Verify against transferred bytes and LCP on a real mobile profile, not on your desktop connection — the entire point is that mobile devices were over-served, and a desktop test hides exactly the case you’re fixing.

VisibilityIQ’s media and performance checks flag images whose intrinsic dimensions far exceed their display size, images missing width/height attributes, lazy-loading applied to above-the-fold or LCP-candidate elements, and pages still serving JPEG where AVIF or WebP would cut transfer substantially — each tied to the specific element and the corrected markup rather than a generic “optimize your images” line. The audit treats format, sizing, and lazy correctness as distinct findings because they have distinct fixes.

Frequently asked questions

AVIF or WebP — which should I use?
Both, in a fallback chain. AVIF produces files roughly 50% smaller than JPEG and meaningfully smaller than WebP at equivalent perceived quality, especially on photographic content with smooth gradients. But AVIF encoding is slower and a small slice of older clients still lack support. WebP is the safer-but-larger middle ground with near-universal support. Serve them via a picture element with the AVIF source first, WebP second, and a JPEG or PNG fallback last; the browser picks the first format it understands. You get AVIF's savings for the clients that support it and graceful degradation for those that don't, with no JavaScript and no user-agent sniffing.
Do width and height attributes still matter if I use CSS?
Yes, and more than ever. Setting explicit width and height attributes on an img element lets the browser compute the aspect ratio and reserve the correct layout box before the image bytes arrive — preventing the layout shift that occurs when an unsized image loads and shoves content down. Modern browsers honor the width and height attributes to derive aspect-ratio even when CSS overrides the rendered size, so you set the intrinsic dimensions as attributes and control display size in CSS. Omitting them is one of the most common causes of Cumulative Layout Shift, and it's a one-line-per-image fix.
When is lazy loading a mistake?
When it's applied to an above-the-fold image, especially the LCP element. loading=lazy tells the browser to defer the fetch until the image nears the viewport — correct for images far down the page, actively harmful for anything visible at load. A lazy-loaded hero image isn't fetched until the browser completes initial layout and confirms it's in view, adding hundreds of milliseconds of resource load delay to your LCP. The rule is mechanical: never lazy-load anything in the initial viewport, and explicitly eager-load or preload the LCP candidate. Lazy-loading is a tool for below-the-fold images and nothing else.