
> This article focuses on Web Performance best practices. Optimising image performance does not replace accessibility: attributes like `alt` are essential and are out of scope for this guide.

The `<img>` element looks simple, but there is a big difference between using it halfway and using it well. In my performance audits it is one of the elements where I find the most room for improvement: images without lazy loading, without declared `width` and `height`, with incorrect priority for the LCP... Mistakes that cost points in Core Web Vitals and, above all, a worse experience for users.

This article covers the most important attributes and techniques, from responsive images to Largest Contentful Paint optimisation.

## Table of Contents

## Responsive images with `srcset` and `sizes`

The `srcset` attribute lets you declare multiple versions of the same image so the browser can pick the most appropriate one based on context (screen density, viewport size).

```html
<!-- ❌ One size for everyone -->
<img src="hero.jpg" alt="Hero image" />

<!-- ✅ Versions for different densities and sizes -->
<img
  src="hero-800.jpg"
  srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
  width="800"
  height="600"
  alt="Hero image"
/>
```

The `sizes` attribute tells the browser how much space the image will occupy in the layout for each breakpoint. Without `sizes`, the browser assumes `100vw` and may download a larger image than needed.

## Modern formats with `<picture>`

Modern formats like AVIF or WebP offer significantly better compression than JPEG or PNG. The `<picture>` element lets you declare multiple sources: the browser loads the first one it supports.

> JPEG XL also promises notable compression improvements, but its browser support is very limited: Chrome dropped it in 2022 and it is only available experimentally in Firefox and Safari. For now, AVIF + WebP is the safest combination.

```html
<!-- ❌ JPEG only, no alternatives -->
<img src="photo.jpg" alt="Photo" />

<!-- ✅ AVIF first, WebP as fallback, JPEG as base -->
<picture>
  <source type="image/avif" srcset="photo.avif" />
  <source type="image/webp" srcset="photo.webp" />
  <img src="photo.jpg" width="800" height="600" alt="Photo" />
</picture>
```

Order matters: the browser tries top to bottom and uses the first format it can render. AVIF offers the best compression but has less support than WebP. JPEG remains the universal fallback.

### Combining responsiveness and modern formats

Both techniques can be combined to deliver the right image in both format and size:

```html
<picture>
  <source
    type="image/avif"
    srcset="photo-400.avif 400w, photo-800.avif 800w, photo-1600.avif 1600w"
    sizes="(max-width: 600px) 100vw, 50vw"
  />
  <source
    type="image/webp"
    srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1600.webp 1600w"
    sizes="(max-width: 600px) 100vw, 50vw"
  />
  <img
    src="photo-800.jpg"
    srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
    sizes="(max-width: 600px) 100vw, 50vw"
    width="800"
    height="600"
    alt="Photo"
  />
</picture>
```

Yes, it is more code. But image CDNs like Cloudinary handle this automatically by detecting the optimal format via the `Accept` request header, without having to write any of these variants by hand.

## Performance attributes: `loading`, `decoding` and `fetchpriority`

These three attributes have the greatest impact on performance metrics and are the ones I most frequently see misconfigured.

### `loading`

Controls when the browser downloads the image.

```html
<!-- ❌ Loads all images upfront, even those outside the viewport -->
<img src="photo.jpg" alt="Photo" />

<!-- ✅ Defers loading of images outside the viewport -->
<img src="photo.jpg" alt="Photo" loading="lazy" />
```

`loading="lazy"` is safe for any image not visible in the initial viewport. For images that are visible — especially the LCP image — use `loading="eager"` (or simply omit the attribute, which is the default).

### `width` and `height`

Declaring dimensions in the HTML allows the browser to reserve space before the image downloads, preventing layout shifts that penalise CLS.

```html
<!-- ❌ No dimensions: layout shifts when the image loads -->
<img src="photo.jpg" alt="Photo" />

<!-- ✅ With dimensions: space is reserved from the start -->
<img src="photo.jpg" alt="Photo" width="800" height="600" />
```

They do not need to match the final display size (CSS handles that). What matters is that the aspect ratio is correct so the browser calculates the reserved space accurately.

### `decoding`

Tells the browser whether it can decode the image asynchronously without blocking the main thread.

```html
<!-- For non-critical images: does not block rendering -->
<img src="photo.jpg" alt="Photo" decoding="async" />

<!-- For the LCP image: forces synchronous decoding -->
<img src="hero.jpg" alt="Hero" decoding="sync" />
```

`decoding="async"` is a good default for secondary images. For the LCP image, `decoding="sync"` ensures it is displayed as soon as possible once downloaded.

### `fetchpriority`

Adjusts the download priority within the browser's priority system.

```html
<!-- ❌ The browser may not prioritise it correctly -->
<img src="hero.jpg" alt="Hero" />

<!-- ✅ Explicitly signals it is critical -->
<img src="hero.jpg" alt="Hero" fetchpriority="high" />

<!-- For non-critical images that might download earlier than needed -->
<img src="banner.jpg" alt="Banner" fetchpriority="low" />
```

`fetchpriority="high"` is especially useful when the LCP image is inside a carousel or appears late in the HTML, and the browser does not detect it as high priority on its own.

## The optimal combination for LCP

For the image that determines the LCP, there are two fronts: download and rendering.

`fetchpriority="high"` raises the download priority, but only once the parser has found the image in the HTML. If the image appears late in the document — inside a component, a carousel, or with a CSS background — the browser may discover it too late.

A `<link rel="preload">` in the `<head>` solves this: it starts the download before the parser reaches the image.

```html
<!-- In the <head>: start the download as early as possible -->
<link rel="preload" as="image" href="hero.jpg" fetchpriority="high" />

<!-- In the <body>: with the correct rendering attributes -->
<img
  src="hero.jpg"
  alt="Hero"
  width="1200"
  height="600"
  loading="eager"
  decoding="sync"
  fetchpriority="high"
/>
```

If using `srcset` with modern formats, the preload tag supports `imagesrcset` and `imagesizes` so the browser preloads exactly the resource it will use:

```html
<link
  rel="preload"
  as="image"
  imagesrcset="hero-400.avif 400w, hero-800.avif 800w, hero-1600.avif 1600w"
  imagesizes="(max-width: 600px) 100vw, 800px"
  fetchpriority="high"
/>
```

And when combined with `<picture>`, modern formats, and `srcset` with `sizes`, the image is well served on all fronts.

## Image CDNs: the pragmatic solution

Maintaining all these format and size variants manually is expensive. Image CDNs like Cloudinary, Imgix or Cloudflare Images automate the heavy lifting: they detect the optimal format for each browser via the `Accept` header, generate the required sizes on demand, and serve the optimised images from the nearest location.

With Cloudinary, for example, it is enough to declare parameters in the URL:

```html
<img
  src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_800/photo.jpg"
  width="800"
  height="600"
  alt="Photo"
  loading="lazy"
  decoding="async"
/>
```

`f_auto` selects AVIF, WebP or JPEG depending on the browser. `q_auto` adjusts quality automatically. All without managing multiple files.

## Conclusion

The `<img>` element has far more potential than we usually tap into. With `srcset` and `sizes` we serve the right size; with `<picture>` and modern formats we reduce file weight; with `width` and `height` we avoid layout shifts; with `loading`, `decoding` and `fetchpriority` we control when and how images load. And for the LCP image, adding a `<link rel="preload">` can be the difference between a green LCP and a red one.

If you want to go deeper into each of these attributes with more examples, I have collected the best practices in the [image-element](https://github.com/nucliweb/image-element) repository.

> To audit the state of images on any page, you can use the [Image Element Audit](https://webperf-snippets.nucliweb.net/Media/Image-Element-Audit) snippet from WebPerf Snippets: run it directly in the DevTools console and it will show you an analysis of the attributes of every `<img>` on the page.
