Optimizing Images for Web Performance

Images make websites look great, but they’re also the biggest performance bottleneck. They add huge file sizes, delay Largest Contentful Paint (LCP), and can even mess with Cumulative Layout Shift (CLS) if they aren’t handled properly. And while developers are quick to optimize JavaScript and CSS, images are often ignored—despite being the heaviest assets on most pages.

So, how do we make images fast, responsive, and efficient? Here’s how:

Choose the Right Image Format

The format of an image has a massive impact on its size and loading speed. Picking the wrong one can easily double or triple your image payload. Check out this illustration: 

Size comparison of images in formats

To the user, it’s the exact same image, but the browser has to download 2x-10x more data depending on the format you pick.

Have a look at this photograph: 

Size comparison of photo in formats

Photographs are quite a bit more complex than illustrations (usually), and the best formats can change. Notice how JPG is smaller than PNG this time, but modern formats like WebP and AVIF are still way smaller.

  • JPG – Best for photos with lots of colors and gradients. Uses lossy compression to keep file sizes small.
  • PNG – Best for graphics, logos, and transparency. Uses lossless compression, but files can be huge.
  • WebP – A modern format that’s often smaller than JPG and PNG while keeping quality high.
  • AVIF – Even better compression than WebP, but not universally supported yet.

good rule of thumbJPG for photos, PNG for graphics, and use WebP or AVIF where possible for modern browsers.

Use Responsive Images

Not all users have the same screen size, so why serve the same massive image to everyone? Responsive images let you deliver the right image size based on the user’s device, reducing unnecessary downloads and improving load times.

Instead of a single <img> tag, try using a <picture> with the srcset and sizes attributes to tell the browser which image to load:

In this example, any screen less than 1400px wide will use an image from the srcset that is at least 100% of the viewport’s width. So if the screen is 1100px wide, the browser will select and download the hero-desktop-1024 version. This automatically scales images to match different devices, saving bandwidth and improving loading speed for smaller screens.

Lazy-Load Below the Fold

One of the worst offenders for slow performance? Loading every image on the page at once—even ones that aren’t visible. This is where lazy-loading comes in. Adding loading="lazy" to an <img> prevents it from downloading until it’s about to be seen.

<img 
  src="downpage-callout.jpg" 
  loading="lazy" 
  height="300"
  width="300"
>  
Code language: HTML, XML (xml)

It’s very important to specify the height and width attributes of images, especially if they are going to be lazy-loaded. Setting these dimensions let’s the browser reserve space in your layout and prevent layout shifts when the content loads. For more about layout shifts and how to prevent them, check out this deep dive on Cumulative Layout Shift.

For images that are critical for rendering, like your LCP element, you should override lazy-loading with fetchpriority="high". This tells the browser to load it ASAP.

<img 
  src="downpage-callout.jpg" 
  fetchpriority="high" 
  height="300" 
  width="300"
>
Code language: HTML, XML (xml)

Serve Images from a CDN

Content Delivery Network (CDN) stores images in multiple locations worldwide, so they load from the nearest server instead of your origin. This speeds up delivery and reduces bandwidth costs.

CDNs use modern HTTP Protocols

Most CDNs will also speed up your images by serving them with modern protocols like HTTP/3, which has significant performance improvements over both HTTP/1 and HTTP/2. Check out this case study on HTTP/3 performance.

HTTP Caching headers

Users always have to download an image at least once, but HTTP caching headers can help repeat visitors load them much faster. HTTP Caching headers instruct the browser to hold onto the image and use it again, rather than asking for it from the CDN again on the next visit. Here’s an example:

Cache-Control: public, max-age=31536000, immutable

This tells the browser that this image won’t change, and that it can be kept locally for 1 year without needing to be requested again. Caching isn’t just for images—it speeds up all static assets. If you’re not sure if your caching is set up correctly, there’s a full guide on HTTP caching that explains how to check and optimize it.

Wrapping Up

Images are one of the biggest opportunities for improving performance. By choosing the right format, compressing efficiently, lazy-loading, and leveraging CDNs with modern protocols, you can massively speed up your site.

If you’re looking for more image optimization tips, with detailed breakdown and real-world examples, check out the complete guide to optimizing website images.

Need to learn about web performance?

5 responses to “Optimizing Images for Web Performance”

  1. Lucas says:

    AVIF – Even better compression than WebP, but not universally supported yet.

    I think that is wrong. If you look at “Can I Use”, you will see:

    I don’t think this is a meaningful difference?

  2. pd says:

    Didn’t realise loading=lazy and fetchpriority=high were universally supported natively.

    Looking at them in isolation like that, they may have been better named like this:

    priority="delayed|immediate"

    … but at least such performance-aiding browser features are now trivial to use. Fantastic!

  3. Thanks for the spreading the word about image performance!

    I have a couple of minor suggestions for article.

    First, PNG24 and PNG32 are lossless, but PNG8 is lossy. PNG8 doesn’t make sense for photographic images, but it can still be useful for line art and other images with a limited color palette. It still has a small place in the web image arsenal.

    Second, the picture element isn’t required in order to use srcset and sizes. The picture element makes the most sense when supporting an art direction use case where you want to switch what the image looks like instead of the more common resolution switching use case.

    If a picture element only has a single source element inside of it and if the source isn’t using a media or type attribute, it shouldn’t use the picture element. In this example, the srcset and sizes attributes can be applied directly to the img element and the rest of the surrounding markup can be deleted.

    I’ve written more on why we shouldn’t use picture most of the time here: https://cloudfour.com/thinks/dont-use-picture-most-of-the-time/

    Finally, it is a small nit, but the article states, “For images that are critical for rendering, like your LCP element, you should override lazy-loading with fetchpriority=’high'”. Yes, you can and sometime should use fetchpriority=”high” for LCP images, but you’re not overriding lazy loading by doing so.

    The example you give is correct. There is no lazyloading attribute on the LCP image which means the browser will load it when it encounters in the document. The fact there is no lazyloading attribute means that there is no lazyloading to override. By adding fetchpriority, you’re suggesting to the browser that this image is higher priority than other assets.

    Again, thank you for spreading the image performance gospel!

  4. toastal says:

    You forgot to mention JPEG XL which is supported in Safari & Firefox Nightly or Librewolf behind the easy-to-enable image.jxl.enabled about:config flag. Unlike the video-codec-based options, JXL offers progressive decoding, lossless JPEG compression, & better clarity for photograph-style raster graphics.

    You can use it now in a <picture> <source> stack on image-set.

  5. John Allsopp says:

    Might I put in a word for the humble SVG? I suspect your sloth wearing sunglasses would be in the order of a few 100 bytes as an SVG.

Leave a Reply

Your email address will not be published. Required fields are marked *

Did you know?

Frontend Masters Donates to open source projects. $363,806 contributed to date.