Code of the Day
AdvancedWeb Performance & Delivery

Image Optimization

Images are typically the largest assets on a page — choosing the right format, size, and loading strategy can halve load time without changing a pixel of visible content.

Web FoundationsAdvanced7 min read
Recommended first
By the end of this lesson you will be able to:
  • Select the right image format for photographs, transparent graphics, and cutting-edge delivery
  • Use the picture element to serve AVIF with WebP and JPEG fallbacks
  • Apply lazy loading and fetchpriority to control download timing
  • Prevent CLS by always reserving space for images before they load

On a typical webpage, images account for more than half of the total bytes transferred over the network. Unlike JavaScript, they run no code; unlike CSS, they affect no layout logic. Yet they are consistently the single biggest contributor to a slow . The good news is that image optimisation is almost entirely a build-time concern — the user pays the cost of a bad decision, but the developer pays no runtime complexity to fix it.

Format selection

Choosing the right container format is the highest-leverage decision. Four formats dominate the web:

FormatCompressionTransparencyUse when
JPEGNoPhotographs
PNGYesIcons, screenshots, line art
WebPBoth modesYesMost images — better than JPEG/PNG at same quality
AVIFLossy (primarily)YesBest available compression; 2023+ browser support

JPEG is the baseline for photographs. WebP delivers 25–35% smaller files than JPEG at equivalent visual quality and has near-universal browser support (96%+ as of 2025). AVIF goes further — another 20–30% smaller than WebP — but still has gaps in older browsers. The practical solution is to offer both and let the browser choose.

The <picture> element for format fallbacks

The <picture> element wraps multiple <source> alternatives. The browser evaluates them top to bottom and picks the first type it supports:

<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="A mountain at sunrise" width="1200" height="800">
</picture>

A browser that understands AVIF downloads hero.avif and ignores the rest. A browser that only understands WebP skips the AVIF source and downloads hero.webp. Every browser understands JPEG, so the <img> src is the final fallback. The user always gets the best format their browser can handle.

The <img> inside <picture> must always have alt, width, and height attributes — the <source> elements do not take these. The <img> also handles all the normal image semantics; the <picture> wrapper is purely about source selection.

Compression: the easy 50%

Even choosing the right format, most images shipped on the web are undercompressed. An image exported from Figma or Photoshop at default settings carries far more data than a viewer can distinguish from an optimised version.

Tools and their typical size reductions:

  • Squoosh (browser-based, interactive) — visual quality slider, immediate before/after comparison; great for learning what quality levels look like.
  • ImageOptim (macOS app) — drag-and-drop lossless optimisation; safe for any image without quality loss.
  • Sharp (Node.js library) — programmatic batch optimisation in a build pipeline; the standard choice for server-side image processing in web projects.
  • imagemin (webpack/Rollup plugin) — runs optimisers automatically at build time; just configure it once.

Typical result: 30–80% file size reduction with no perceptible quality change. A 1.5 MB JPEG hero becomes 400 KB without a pixel of visible difference at 1440px viewport width.

Responsive images: the right size for each screen

Even an optimised image can be wasteful if the browser downloads a 2400 px wide file to display it in a 400 px column on a mobile screen. srcset and sizes let the browser pick the right resolution:

<img
  src="hero-800.jpg"
  srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="A mountain at sunrise"
  width="1600"
  height="1067"
>

The browser reads sizes to know how wide the image will be rendered (100% of the viewport on narrow screens, 50% on wide ones), then picks the srcset candidate that best matches the display size and . On a 2x Retina display at 400 px CSS width, it fetches the 800 px file — crisp, but not the wasteful 1600 px file.

Lazy loading

Images below the fold do not need to start downloading the moment the HTML is parsed. The loading="lazy" attribute tells the browser to defer the download until the image is about to enter the viewport:

<img
  src="section-photo.jpg"
  loading="lazy"
  alt="Team members at the company offsite"
  width="800"
  height="600"
>

is handled entirely by the browser with no JavaScript. It reduces the initial page weight — bytes that would have been downloaded on page load are now deferred until they are needed. For pages with many below-the-fold images this is one of the highest-ROI optimisations available.

Never apply loading="lazy" to the LCP image. The hero image must load as fast as possible; deferring it guarantees a poor LCP score.

fetchpriority — push the LCP image to the front

Even without loading="lazy", the browser treats all images as medium priority by default. During the critical path, it competes for bandwidth with CSS, scripts, and fonts. The fetchpriority="high" attribute tells the browser to promote this specific image to the top of the download queue:

<img
  src="hero.webp"
  fetchpriority="high"
  alt="A mountain at sunrise"
  width="1200"
  height="800"
>

Combined with a <link rel="preload"> in <head>, this ensures the LCP image starts downloading at the earliest possible moment and is prioritised over competing resources:

<head>
  <link rel="preload" as="image" href="hero.webp" fetchpriority="high">
</head>

Preventing CLS with explicit dimensions

When the browser encounters an <img> without width and height attributes, it cannot reserve space in the layout before the image loads. It renders the surrounding text first, then jumps everything down when the image arrives — a textbook event.

The fix is simple: always declare the image's intrinsic dimensions:

<!-- bad: no dimensions, layout shifts when image loads -->
<img src="photo.jpg" alt="...">

<!-- good: browser reserves exact space before download completes -->
<img src="photo.jpg" alt="..." width="1200" height="800">

If your layout uses CSS to make images fluid (max-width: 100%; height: auto), the width and height attributes still help — the browser uses their ratio to compute the aspect-ratio and reserve proportional space, even when the CSS constrains the rendered width.

The aspect-ratio CSS property is an alternative approach for images where the intrinsic dimensions are not known at authoring time (for example, user-uploaded content). Set aspect-ratio: 16/9; width: 100%; height: auto to reserve the correct slot before the image loads.

Where to go next

Images handled, the next most common performance liability on the web is fonts. Font Loading Strategies explains the font-display descriptor, preloading, font subsetting, and when the system font stack is the right answer.

Finished reading? Mark it complete to track your progress.

On this page