Back to top

Image optimisation

Web performance series - part 4

Recently I had a chance to present a talk at NDC Sydney about web performance and it received a great feedback.

That inspired me to write up a series of posts on each topic I covered in that talk, and who knows, maybe each of these posts would be a talk some day by their own 😃.

All other parts:

Part 1 on HTML and CSS

Part 2 use Preload/Prefetch to boost load time

Part 3 JavaScript tips and tricks

Part 5 Web font optimisation

Intro

Believe me you don’t want Google to hate your website. Knowing the most heavy weight items on every website are images, it is very important to focus on their optimisation.

Fortunately, reducing their size is very easy and has massive impact on the overall size of the page. For most of the users on a mobile device, the image quality wouldn’t be so important. Even on a desktop with high resolution there are sacrifices you can make. That’s because human eyes won’t detect certain pixels not being present.

As long as you don’t go too far optimising an image to make it ugly, it is a good to continue reducing its size 🤷‍.

Images take more than %50 of a web page’s overall weight 😱. That’s why we should optimise them!

Average bytes per page
Average bytes per page 2018

What does image optimisation mean

Image optimisation is referred to reducing image sizes using different techniques. This will result in the page being loaded faster, hence having a better user experience.

Why bother

Here are some benefits you would gain by optimising your images:

  • It will improve the page load time. For every second users have to wait for the page to load, Amazon will lose $1.6 billion in sales per year (you may now pick a calculator and see how much your company would be impacted 😁).
  • It improves your SEO capabilities, your site will rank higher in search engines resulting in more traffic.
  • Creating site backups will be faster (if you are using a CMS or if you backup the whole site)
  • Smaller image sizes use less bandwidth, resulting in not draining user’s mobile data quota.
  • Requires less storage space on the server (or CDN), makes it more cost effective.

Let’s optimise them

The primary goal of image optimisation is to find the balance between file size and image quality. But before we start on optimisation tips, we should understand different image formats and when to use each.

Choose the right format

  • PNG – produces higher quality images, but also has a larger file size. Was created as a lossless image format, although it can also be lossy.
  • JPEG – uses lossy and lossless optimization. You can adjust the quality level for a good balance of quality and file size.
  • GIF – only uses 256 colors. It’s the best choice for animated images. It only uses lossless compression.

There are some newer image formats like WebP and Jpeg2000, but the browser support is not there yet. In summary you should use JPEG for images with lots of colour and PNG for simpler images.

Size vs Compression

This is an example of an image before and after compression. Note how the quality is impacted (you can’t see it in the cat, but around it):

Before Cat

After Cat

For the same reason that you can’t figure out the difference in the cat itself between the first and second image, it is safe to compress image to that degree. Just to let you know the first image is 4mb and the second is only 27kb. 🤷‍

Lossy vs Lossless Optimisation

Now that you know how important it is to compress images and reduce the quality, it is also important to know we have two types of compression:

  • Lossy - this is kind of a filter to eliminate some data from the image, in the above example you could see some loss in the area around the cat. Using this technique, the file size will be reduced to a large degree. Tools such as Adobe Photoshop, Affinity Photo or some free online tools such as Image Compressor will do the trick for you.

  • Lossless - this is a filter that does not eliminate any data, but uses compression only to reduce the size, but it means it requires images to be uncompressed to operate. You can do this easily using tools like FileOptimizer and ImageOptim.

You will need to experience it yourself and find the sweet spot for your images. It is a task which requires some work beforehand but saves you a lot of time later. Another thing to consider is to use these tools like ImageOptim in your build process, this way you don’t even need to worry about doing it upfront. And your original images remain untouched.

Using the right dimension

As important as compression is, by itself it can go only so far keeping the same dimension. After you apply the compression you cannot reduce the size with the same width and height anymore.

Apart from this, you will need to know that showing a picture with 2000px width on a mobile device is not such a good idea. Especially on smaller devices the human ability to detect changes are far less than when they are looking at a bit monitor with a large aspect ratio.

To achieve this you can use the srcset and width descriptors attribute in HTML. With this, you can mention multiple screen sizes and specify which image to use for each size.

Cat picture using srcset

When you use width descriptors, you’re providing the browser with a list of images and their true width so that it can select the best source based on the viewport size.

Using SVGs

SVG is a scalable vector format which works great for logos, icons, text, and simple images. Here are a couple reasons why you would consider using them:

  • SVGs are automatically scalable in both browsers and photo editing tools. This is a dream for a web and graphic designers!
  • Google indexes SVGs, the same way it does PNGs and JPGs, so you don’t have to worry about SEO.
  • SVGs are traditionally (not always) smaller in file size than PNGs or JPGs. This can result in faster load times.

Here is an example to show you how much difference it can have (Images from https://genkihagata.com):

JPEG
JPG
Size: 81.4KB

PNG
JPG
Size: 85.1KB

SVG
Size: 6.1KB

Lazy loading images

When we consider all we’ve gone through so far, you would realise at some point that it is not enough to make images smaller anymore. Especially if you have too many of them in the page. This is where we need to make sure our web page loads with them fast instead.

This is where lazy loading comes to rescue. Let’s see a demonstration on how it works (video from CSS Tricks):

What is it?

Lazy loading images is simply the act of not loading images until a later point in time. It is a technique in web development which applies to many other form of resources, but here we are focusing on images.

Wikipedia: Lazy loading is a design pattern commonly used in computer programming to defer initialization of an object until the point at which it is needed. It can contribute to efficiency in the program’s operation if properly and appropriately used.

How it is done

Imagine you have a very long page with a lot of images. Why would the image at the bottom of the page get loaded if the user cannot see it. As simple as that, you can load the image on an event like when that part of the page is visible (using scroll event handler) or any other event. But not just when the page loads.

Apart from that, if the user never scrolls down, that image wouldn’t get loaded, resulting in saving some network traffic and data usage for the end user.

You will start to see a lot of benefits considering how much impact this has on the overall page load time and speed.

Lazy loading techniques

There are two common ways of loading an image on a page, using an img tag, and CSS background-image. Let’s start with the image tag.

Image tag

Here is a simple image tag we normally use to load an image:

Copy
<img src="/path/to/some/cat/image.jpg" />

The markup for lazy loading images is pretty similar. The src attribute is the trigger for the browser to send a network request and fetch the image. No matter if this is the first or the 50th image on your page.

To defer the load, simply use data-src attribute.

Copy
<img data-src="/path/to/some/cat/image.jpg" />

Since the src is empty the browser doesn’t load the image when the tag is rendered. Now it is just the matter of triggering the load which normally is done when the image is entered the viewport.

We can use events like scroll, resize, and orientationChange to figure out when to trigger the load. The scroll event is pretty clear, when the user scrolls if the image tag is on the page then we trigger the load and tell the browser to fetch the image. However, the resize and orientation change events are equally important. The resize is when the user changes the window size like when they make the window smaller. The orientation change happens when the user rotates their device.

Once we hook into these events, we can enable lazy loading and the result is really good:

Copy
document.addEventListener(
  'DOMContentLoaded',
  function() {
    var lazyloadImages = document.querySelectorAll(
      'img.lazy'
    )
    var lazyloadThrottleTimeout

    function lazyload() {
      if (lazyloadThrottleTimeout) {
        clearTimeout(lazyloadThrottleTimeout)
      }

      lazyloadThrottleTimeout = setTimeout(
        function() {
          var scrollTop = window.pageYOffset
          lazyloadImages.forEach(function(img) {
            if (
              img.offsetTop <
              window.innerHeight + scrollTop
            ) {
              img.src = img.dataset.src
              img.classList.remove('lazy')
            }
          })
          if (lazyloadImages.length == 0) {
            document.removeEventListener(
              'scroll',
              lazyload
            )
            window.removeEventListener(
              'resize',
              lazyload
            )
            window.removeEventListener(
              'orientationChange',
              lazyload
            )
          }
        },
        20
      )
    }

    document.addEventListener('scroll', lazyload)
    window.addEventListener('resize', lazyload)
    window.addEventListener(
      'orientationChange',
      lazyload
    )
  }
)

Using intersection API

Let’s see what this API offers:

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.

Opposite to the previous technique where you might see some performance impact on the page because of all of those event handlers, this approach is relatively new.

This API removes the previous performance hit by doing the math and delivering a very efficient way to call a callback function when the resource is on screen:

Copy
document.addEventListener(
  'DOMContentLoaded',
  function() {
    var lazyloadImages

    if ('IntersectionObserver' in window) {
      lazyloadImages = document.querySelectorAll(
        '.lazy'
      )
      var imageObserver = new IntersectionObserver(
        function(entries, observer) {
          entries.forEach(function(entry) {
            if (entry.isIntersecting) {
              var image = entry.target
              image.src = image.dataset.src
              image.classList.remove('lazy')
              imageObserver.unobserve(image)
            }
          })
        }
      )

      lazyloadImages.forEach(function(image) {
        imageObserver.observe(image)
      })
    } else {
      var lazyloadThrottleTimeout
      lazyloadImages = document.querySelectorAll(
        '.lazy'
      )

      function lazyload() {
        if (lazyloadThrottleTimeout) {
          clearTimeout(lazyloadThrottleTimeout)
        }

        lazyloadThrottleTimeout = setTimeout(
          function() {
            var scrollTop = window.pageYOffset
            lazyloadImages.forEach(function(img) {
              if (
                img.offsetTop <
                window.innerHeight + scrollTop
              ) {
                img.src = img.dataset.src
                img.classList.remove('lazy')
              }
            })
            if (lazyloadImages.length == 0) {
              document.removeEventListener(
                'scroll',
                lazyload
              )
              window.removeEventListener(
                'resize',
                lazyload
              )
              window.removeEventListener(
                'orientationChange',
                lazyload
              )
            }
          },
          20
        )
      }

      document.addEventListener(
        'scroll',
        lazyload
      )
      window.addEventListener('resize', lazyload)
      window.addEventListener(
        'orientationChange',
        lazyload
      )
    }
  }
)

We attach the observer on all the images we want to be lazy loaded. When the API detects that the element has entered the viewport, using the isIntersecting property, we pick the URL from the data-src attribute and move it to the src attribute for the browser to trigger the image load like before. Once this is done, we remove the lazy class from the image and also remove the observer from that image.

CSS background image

CSS background images are not as straightforward as the image tag. To load them the browser needs to build both the DOM tree and CSSDOM tree (see here). If the CSS rule is applicable to the node the browser loads it, otherwise doesn’t. So all we need to do is to not give it a background property by default and add it when it’s visible:

Copy
document.addEventListener(
  'DOMContentLoaded',
  function() {
    var lazyloadImages

    if ('IntersectionObserver' in window) {
      lazyloadImages = document.querySelectorAll(
        '.lazy'
      )
      var imageObserver = new IntersectionObserver(
        function(entries, observer) {
          entries.forEach(function(entry) {
            if (entry.isIntersecting) {
              var image = entry.target
              image.classList.remove('lazy')
              imageObserver.unobserve(image)
            }
          })
        }
      )

      lazyloadImages.forEach(function(image) {
        imageObserver.observe(image)
      })
    } else {
      var lazyloadThrottleTimeout
      lazyloadImages = document.querySelectorAll(
        '.lazy'
      )

      function lazyload() {
        if (lazyloadThrottleTimeout) {
          clearTimeout(lazyloadThrottleTimeout)
        }

        lazyloadThrottleTimeout = setTimeout(
          function() {
            var scrollTop = window.pageYOffset
            lazyloadImages.forEach(function(img) {
              if (
                img.offsetTop <
                window.innerHeight + scrollTop
              ) {
                img.src = img.dataset.src
                img.classList.remove('lazy')
              }
            })
            if (lazyloadImages.length == 0) {
              document.removeEventListener(
                'scroll',
                lazyload
              )
              window.removeEventListener(
                'resize',
                lazyload
              )
              window.removeEventListener(
                'orientationChange',
                lazyload
              )
            }
          },
          20
        )
      }

      document.addEventListener(
        'scroll',
        lazyload
      )
      window.addEventListener('resize', lazyload)
      window.addEventListener(
        'orientationChange',
        lazyload
      )
    }
  }
)

And:

Copy
#bg-image.lazy {
  background-image: none;
  background-color: #f1f1fa;
}
#bg-image {
  background-image: url('path/to/some/cat/image.jpg');
  max-width: 600px;
  height: 400px;
}

Summary

We’ve seen how to reduce the image size using different compression methods, how to load different sizes for different screen sizes and at last how to lazy load them. Using these techniques, you can improve the performance of the page so much it becomes a hobby for you after some time to play with.

And as always please spread the word and behold for the next post on web fonts 😃👋.

Support my work 👇🏽
Crypto