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 2 use Preload/Prefetch to boost load time
Part 3 JavaScript tips and tricks
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 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.
[Update]
The support for WebP and Jpeg2000 has improved a lot, so feel free to use them. However, make sure you have a fallback option in case it's not supported, you don't want to exclude users from viewing those.
<picture>
<source srcset="img/awesomeWebPImage.webp" type="image/webp">
<source srcset="img/creakyOldJPEG.jpg" type="image/jpeg">
<img src="img/creakyOldJPEG.jpg" alt="Alt Text!">
</picture>
The above snippet is the best combination to support all browsers, old and new, even those which don't support picture.
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):
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.
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 |
---|
Size: 81.4KB |
PNG |
---|
Size: 85.1KB |
SVG |
---|
Size: 6.1KB |
Note: I can't embed an svg in this post so I just used the the jpeg file. But you can check it on my original post.
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:
<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.
<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:
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:
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:
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:
#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 😃👋.
Top comments (11)
I put the cat images in a JuxtaposeJS to make it easier to compare them.
But I don't see a difference, are you sure you uploaded the original (before) image? I would expect the color banding in the background to be only in the (more) compressed version.
Edit: your original article had the correct files. Now you can see the difference.
Yeah I might have messed up the uploads, will have a look and fix it. Thanks
Done
One good resource I like for image optimisation is TinyPNG. Don't let the name fool you - it supports JPG and PNG.
Has an API that you can integrate with a few different ways. Found it very useful integrated into content management systems for client websites.
Don't forget that coming soon will be native browser lazy loading via an html attribute. While its only behind a feature flag right now, there is literally no harm in having it in place in your code already.
addyosmani.com/blog/lazy-loading/
I would disagree that webP has not enough support. It works everywhere except safari and iOS.
Given that there are a lot of ways to control what format is loaded and to provide alternative fallbacks I think strategies that exclude newer approaches are not addressing the full potential.
You're completely right Benjamin, although the support is not there yet. I updated the post and added a snippet to show how to add WebP and have a fallback 😊
The links to the other parts in the series do not seem to work : (
Fixed
Absolutely fabulous writeup.
Thank You!