Skip to content

Illustration by unDraw

Optimised images for the web, responsive and adaptive

Images are one of the biggest performance bottlenecks — but only if you let them be.

In this short piece, I’ll take you through some common image-optimisation techniques you can implement right now. Hopefully, you’ll get huge performance improvements and create a smooth web experience.

But first, let’s get on with some data about why it’s important to optimise your images on the web — and especially for mobile users.

The average mobile website is 2.2 MB big, of which 68% are images.

“As we have just tested, a very high percentage, around 70% of a site is composed of images.” - Mustafa Kurtuldu (source)

In a slow 3G network, big resource size would be a huge bottleneck for page loading time. Trying to download only a few unoptimised images will keep the network busy, preventing other equally important resources from being downloaded.

Also, according to Facebook Blueprint, as many as 40% of users abandon the site at a three-second delay. When you add this on top of the previous point on image size, it’s easy to realise we need to be careful in how we serve images to our users.

It’s also about time we think of image optimisation as an important step to a good user experience on the mobile web and try to be mindful about the size of images we’re serving our users.

Before we begin, bear in mind websites differ from each other — as each website may respond with higher performance increase or even have a reverse effect (!) depending on the performance metrics you are tracking.

Best practise: Make sure to test after every optimisation in order to keep track of what’s working for your website and what’s not.

Image compression#

This part may be the most important of all, and this is the reason why it’s first on the list.

Now the Design and Marketing departments are outrageously jumping onto their offices shouting, “Oh no way I am touching my beautiful images! I don’t want to have crappy quality just to save a few KBs.”

But don’t take anybody's word for it.

Try compressing and comparing a few images using the squoosh app. Or even better, try compressing some images and then ask the deniers to point out the compressed ones. You’ll be able to see it for yourself that rarely there’s an obvious difference between quality of uncompressed and lossy optimised images.

After the deniers are out of the way, come back here because I have some resources for you that you might be interested in.

Warning: Try not to recompress (video). It’s recommended to always compress from the original image to avoid excess noise.

Responsive and Next-Gen Format Support#

Next, we’ll take a look at how we can use the <picture> HTML tag to serve different image sizes, resolutions, and formats depending on the platform that’s requesting them.

Sounds complicated? Bare with me while I go through the steps.

The <picture> element#

I’ve included a bit of an explanation here if you’re not familiar with the <picture> element. Feel free to skip this part if you’re already familiar.

Inside the <picture> element, we define all our images to match different cases depending on the device. The <picture> element will try to deduce the best match for the current device, starting from top-most option and falling back to options below. If none are there, it’ll fallback to the last <img> element.

The <img> element takes the source of the image URL to be served, the styling, the size, and the alt text.

Multiple screen-size support#

Let’s kick off by looking at how we can support various image sizes for mobile, tablet and desktops. We’ll be using the media attribute of the <source> tag placed inside a <picture> tag and define our media queries there:

<picture>
  <source srcset="img-1024.jpg" type="image/jpg" media="(min-width: 1024px)" />
  <source srcset="img-768.jpg" type="image/jpg" media="(min-width: 768px)" />
  <source srcset="img-480.jpg" type="image/jpg" media="(min-width: 480px)" />
  <img srcset="img-320.jpg" type="image/jpg" alt="A beautiful responsive image" />
</picture>

As you can see above, we can serve a scaled-down version of the image for mobiles with 320px and 480px of maximum screen width, a 768px-wide picture for tablets and above and finally a 1024px-wide picture for desktops.

Best practise: Please be mindful of the alt attribute, and include it in the last tag inside of the <picture> element — which is the <img> tag.

Multiple screen-resolution support#

Moving on to the second part of making our images awesome on the web, we’ll be including more versions of our images with higher resolution for high-DPI screens. For this trick, we’ll be leveraging some ninja power the srcset attribute gives us.

Aren’t you already bored of generating a bunch of images?
I feel you. Just use a tool of your choice (e.g. ImageMagick), and let there be automation! Otherwise, if you have the dough, you can use Cloudinary, which offers a great service, as an alternative. Many devs find Cloudinary an easier solution to images.

<picture>
  <source srcset="img-1024.jpg, img-1024-2.jpg 2x" type="image/jpg" media="(min-width: 1024px)" />
  <source srcset="img-768.jpg, img-768-2x.jpg 2x" type="image/jpg" media="(min-width: 768px)" />
  <source srcset="img-480.jpg, img-480-2x.jpg 2x" type="image/jpg" media="(min-width: 480px)" />
  <img srcset="img-320.jpg, img-320-2x.jpg 2x" type="image/jpg" alt="A beautiful responsive image" />
</picture>

For example, in line 4, we simply state that for devices:

  • for screens wider than 480px (media attribute)
  • and 2 times (2x) the base DPI (96) (srcset attribute)
  • use the high-res image img-480–2x.jpg
  • otherwise if the screen has a base DPI
  • use the lower-res one img-480.jpg

Images in next-gen format support#

Wrapping up, to include all of the above responsive options and include additional images in a next-gen format, use the following:

<picture>
  <source srcset="img-1024.webp, img-1024-2x.webp 2x" type="image/webp" media="(min-width: 1024px)" />
  <source srcset="img-1024.jpg, img-1024-2.jpg 2x" type="image/jpg" media="(min-width: 1024px)" />
  
  <source srcset="img-768.webp, img-768-2x.webp 2x" type="image/webp" media="(min-width: 768px)" />
  <source srcset="img-768.jpg, img-768-2x.jpg 2x" type="image/jpg" media="(min-width: 768px)" />
  
  <source srcset="img-480.webp, img-480-2x.webp 2x" type="image/webp" media="(min-width: 480px)" />
  <source srcset="img-480.jpg, img-480-2x.jpg 2x" type="image/jpg" media="(min-width: 480px)" />
  
  <source srcset="img-320.webp, img-320-2x.webp 2x" type="image/webp" />
  <img srcset="img-320.jpg, img-320-2x.jpg 2x" type="image/jpg" alt="A beautiful responsive image" />
</picture>

Here I’m including images in WebP format if they’re supported by the browser. Instead, the browser will fallback to the next JPG image.

Best practise: Don’t forget the type="image/webp" part because, in my experience, the WebP image will not be picked up by the browser.

It looks like a handful, but I promise that was it for adding responsive and next-gen format support for your images. It is quite a huge thing to remember so I saved this code in a gist for easy sharing and referencing.

Lastly, make sure to have automation in place so it’ll be easy to update images later on.

If you’re interested, check out my other article about using WebP, WebM and AVIF instead of JPG, PNG, and GIF files on the web for better performance.

The display: none trap#

Sometimes, we’d like to hide images from loading initially when the user lands on our page and instead lazily or conditionally load images. But if you’re not familiar with the quirks of the <img> element, you may fall in the display: none trap without even noticing.

As a result, you might still be hurting performance as the image might still be requested by the browser, wasting precious bandwidth and KBs.

Example 1#

Does display: none avoid triggering a request to the /example.jpg URL?

<div style="display:none">
  <img src="example.jpg">
</div>

No. The image will still be requested.

Here, a JavaScript library isn’t reliable for lazy loading if display: none is used, as the image will be requested before JavaScript can alter the src.

That’s why you’ll see the data-src attribute used in most lazy-loading JS libraries.

Example 2#

Does display:none on a <div> avoid triggering a request for a background: url()?

<div style="display:none">
  <div style="background: url(example.jpg)"></div>
</div>

Yes.

CSS backgrounds aren’t fetched as soon as an element is parsed. Calculating CSS styles for children of elements with display: none would be less useful as they don’t impact the rendering of the document. Background images on child elements aren’t calculated or downloaded.

Prevent downloads on mobile#

Whichever way you’re going to prevent the image from being downloaded, make sure to protect mobile first as it’s the most vulnerable device due to lower CPU and, more often, less network bandwidth and higher latency on 3g/4g.

.mobile-image {
  background: url(example.jpg);
}

@media only screen and (max-width: 768px){
  .mobile-image {
    background-image: none; 
  }
}

By including a media query that’ll replace background-image: url() to none for mobiles, we’re making sure images intended for bigger devices aren’t downloaded on mobile.

Native image lazy loading#

Native image lazy loading has been supported on Chrome since version 75. That means you no longer need to use complex image lazy loading libraries to achieve the same effect in modern browsers.

Native lazy-loading demonstration by Addy Osmani on YouTube

You’re probably already using a script like lazysizes to lazy load images; however, there’s a way to still lazy load images while shipping less code and executing less JavaScript at the client. Plus it’s SEO-friendly. All these pluses, of course, depend on the browser support. At the time of writing, the loading attribute is supported on Chromium-based browsers, Firefox and it is behind a flag on Safari 13.1+.

Warning: Image duplicates are rather common in websites (i.e. downloading multiple sized images on a single load on a single device). Make sure you’re not using more than one script for lazy loading. Check the Network tab in DevTools to check which images are being downloaded.

Shimmed native image lazy loading#

Source

<!-- Let's load this in-viewport image normally -->
<img src="hero.jpg" alt="" />

<!-- Let's lazy-load the rest of these images -->
<img data-src="unicorn.jpg" alt="" loading="lazy" class="lazyload" />
<img data-src="cats.jpg" alt="" loading="lazy" class="lazyload" />
<img data-src="dogs.jpg" alt="" loading="lazy" class="lazyload" />

<script>
  if ('loading' in HTMLImageElement.prototype) {
    const images = document.querySelectorAll('img[loading="lazy"]');
    images.forEach(img => {
      img.src = img.dataset.src;
    });
  } else {
    // Dynamically import the LazySizes library
    const script = document.createElement('script');
    script.src =
      'https://cdnjs.cloudflare.com/ajax/libs/lazysizes/4.1.8/lazysizes.min.js';
    document.body.appendChild(script);
  }
</script>

If you’d like to learn more and look at a few examples, check out this article by Addy Osmani. Here’s an example website as well if you fancy.

Conclusion#

Hope you found these techniques insightful and start optimising your images today. Make sure to check them out, and see if you get any performance improvements, especially if your website is image-heavy.

Bear in mind, though, that it’s a best practise to measure performance before and after each change because every website is different and some optimisations will work, while others won’t.

Have a good day. Cheers!