The Practical Developer

Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want

Most websites ship 2 MB hero images for slots that render at 600 px wide. The fix is half configuration and half discipline: a CDN that does on-the-fly format negotiation, srcset that picks the right size, and the four `<img>` attributes that move every PageSpeed metric.

A laptop open on a desk — the right place to inspect a slow image and shave 90% off its bytes

The dashboard ships a hero image that is 2.4 MB. The slot renders at 600 px wide on the average device. The browser downloads a 4000-px-wide JPEG, decodes it, scales it down, and discards the rest. Every visitor pays for the difference. Lighthouse is yelling about LCP and “properly size images” and “serve images in next-gen formats.” The fix is not “compress harder.” It is to ask the CDN for the right size in the right format, and to teach the <img> tag to pick the right variant for the actual device.

Image optimization in 2023 is mostly about three things: an image CDN that does the work, srcset so the browser picks the right size, and the four <img> attributes that PageSpeed cares about. This post is the working pattern.

The image CDN does the heavy lifting

A modern image CDN — Cloudinary, Imgix, ImageKit, Cloudflare Images, AWS CloudFront with Lambda@Edge, or your own with imgproxy — accepts URL parameters and returns the variant you ask for:

https://cdn.example.com/path/to/image.jpg?w=800&q=75&fm=auto

The host caches on (URL, headers) so the same variant is generated once and served forever. The benefit is enormous:

  • One source image; many derived variants.
  • Format negotiation: serve AVIF / WebP / JPEG based on Accept header.
  • On-the-fly resize: no need to pre-render every size.

Without a CDN, image optimization is a build-time pipeline that can never quite keep up with new content. With a CDN, every image is automatically right-sized.

The four URL params worth knowing

(Specifics vary by CDN; the concepts are universal.)

  • w= width. The pixel width to render at. Match this to the slot the image fills, not the source resolution.
  • q= quality. 75 is the sweet spot. Above 85 the file size doubles for no perceptual gain. Below 70 starts to look bad on photos.
  • fm=auto or auto=format. Negotiate format based on Accept header — AVIF for newer browsers, WebP for older, JPEG for ancient. Strictly better than picking one format yourself.
  • fit=crop / fit=contain. How to handle aspect ratios that don’t match.

Avoid q=100 — every byte over q=85 is wasted. Avoid fm=webp (or fm=avif) directly — auto handles the fallback for old browsers correctly.

srcset: the browser picks the size

A single fixed-width URL is wrong because devices differ. A 600px slot on a retina iPhone 14 needs a 1200px image (2x DPR); a 600px slot on a 1080p laptop needs a 600px image. srcset lets the browser pick:

<img
  src="https://cdn.example.com/photo.jpg?w=600&q=75&fm=auto"
  srcset="
    https://cdn.example.com/photo.jpg?w=600&q=75&fm=auto 600w,
    https://cdn.example.com/photo.jpg?w=1200&q=75&fm=auto 1200w,
    https://cdn.example.com/photo.jpg?w=2000&q=75&fm=auto 2000w
  "
  sizes="(max-width: 768px) 100vw, 600px"
  width="600"
  height="400"
  alt="..."
  loading="lazy"
  decoding="async"
/>

srcset lists candidates with their intrinsic widths (600w, 1200w). sizes tells the browser the rendered width of the slot under different conditions. The browser computes (DPR × rendered width) and picks the smallest candidate ≥ that target.

The biggest mistake: omitting sizes. Without it, browsers may pick the largest candidate to be safe — defeating the point.

The four <img> attributes that move scores

Every PSI hint about images comes back to these four:

width and height. Required to prevent CLS (Cumulative Layout Shift). The browser reserves space before the image loads. Match the aspect ratio; the actual rendered size can still be CSS-controlled.

loading="lazy" for everything below the fold. The browser defers loading until the image is near the viewport. Do not lazy-load the LCP image — it is the largest contentful paint, which is exactly what you want to load fast.

decoding="async". Decoding a 2 MB JPEG can block the main thread for 30 ms. decoding="async" tells the browser to do it off-thread.

alt. Required for accessibility. PSI flags missing alts. Be descriptive (alt="developer at a keyboard"), not generic (alt="image").

The LCP image needs special handling

The hero image at the top of the page is almost always the LCP element. Treat it differently:

<img
  src="https://cdn.example.com/hero.jpg?w=1200&q=75&fm=auto"
  srcset="..."
  sizes="..."
  width="1200" height="675"
  alt="..."
  loading="eager"
  fetchpriority="high"
  decoding="async"
/>

loading="eager" (or omitted — eager is the default) forces immediate load. fetchpriority="high" (Chromium-only but widely supported) tells the browser to prioritize this fetch over others.

For an extra speedup, add a <link rel="preload"> in the head:

<link
  rel="preload"
  as="image"
  href="https://cdn.example.com/hero.jpg?w=1200&q=75&fm=auto"
  imagesrcset="..."
  imagesizes="..."
/>

This starts the fetch the moment the HTML is parsed — earlier than the parser would discover the <img> tag.

Network hints in the head

Set up the CDN connection early:

<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link rel="dns-prefetch" href="https://cdn.example.com" />

preconnect does DNS, TCP, and TLS up front. Save 100–300 ms on first image fetch. dns-prefetch is a fallback for browsers that don’t support preconnect.

AVIF, WebP, and the format wars

In 2023, the browser support is:

  • AVIF: ~90% of users (Chrome, Firefox, Safari 16+). 30-50% smaller than WebP at equivalent quality.
  • WebP: ~97% of users. 25-35% smaller than JPEG.
  • JPEG: 100%. The fallback.

auto=format (or fm=auto) on the CDN side handles the negotiation. Don’t hard-code a format or you cut yourself off from future improvements.

For large hero images, AVIF saves real bytes. A 1200×675 JPEG at q=75 is ~110 KB; AVIF at q=75 is ~50 KB. For thumbnails (~600 KB), the difference is smaller in absolute terms but still meaningful.

What to watch in Lighthouse

The image-related audits to make pass:

  • “Properly size images” — pass with srcset + sizes.
  • “Serve images in next-gen formats” — pass with auto=format.
  • “Efficiently encode images” — pass with q=75-80.
  • “Defer offscreen images” — pass with loading="lazy".
  • “Image elements have explicit width and height” — pass with the attributes.
  • “Largest Contentful Paint” — pass with eager + fetchpriority + preload on hero.

A site that gets all of these green typically has a Performance score above 90 on real devices.

Common mistakes

Hard-coding fm=webp. Safari ate up the WebP cost; AVIF is now better. fm=auto keeps you current.

Lazy-loading the hero. Lighthouse will mark LCP as eager-loaded even if the attribute is lazy. Use loading="eager".

Tiny placeholders that block layout. A 1×1 placeholder image with width="1" height="1" causes the same CLS as missing dimensions. Use the real width/height for the slot.

Pasting a Unsplash page URL. https://unsplash.com/photos/... is the website, not the CDN. The CDN host is images.unsplash.com. Always extract the photo URL.

Build-time vs runtime optimization

Two camps:

  • Build-time: Eleventy Image, Next.js next/image, Astro @astrojs/image. Generates variants at build, ships them as static files. Fast, deterministic, but doesn’t optimize user-uploaded content.
  • Runtime: Cloudinary, Imgix, etc. Generates variants on demand. Handles user uploads, but costs money per transformation.

Most production sites use both: build-time for editorial content, runtime CDN for user uploads. The framework you choose usually has an answer for one of these; pick the matching tool.

The takeaway

Image performance is solved. The pieces are: an image CDN that does format and size on the fly, srcset + sizes so the browser picks the right variant, the four <img> attributes that PSI checks, eager + preload for the LCP image. Implement once, document the convention, never think about it again.

The next time PageSpeed complains about images, check the four attributes. Nine times out of ten the fix is one missing sizes or one hardcoded fm=webp.


A note from Yojji

The kind of front-end performance work that takes a heavy site to a 90+ Lighthouse score — image CDN config, srcset disciplines, preload hints in the right places — is the kind of polish Yojji’s frontend teams build into the products they ship for clients.

Yojji is an international custom software development company founded in 2016, with teams across Europe, the US, and the UK. They specialize in the JavaScript ecosystem (React, Node.js, TypeScript), cloud platforms (AWS, Azure, GCP), and full-cycle frontend engineering — including the image and asset optimization that decides whether a site feels slow or instant.