Skip to content

Practical guide to the <img> element: from the basics to LCP

Published:

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

Open 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).

<!-- ❌ 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.

<!-- ❌ 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:

<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.

<!-- ❌ 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.

<!-- ❌ 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.

<!-- 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.

<!-- ❌ 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.

<!-- 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:

<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:

<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 repository.

To audit the state of images on any page, you can use the 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.


Previous Post
SVGs on the web: performance comparison based on how you load them
Next Post
Yield to Main: setTimeout vs queueMicrotask vs scheduler.postTask