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
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 |
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.
Here are some benefits you would gain by optimising your images:
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.
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.
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. 🤷
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.
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.
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:
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 |
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):
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.
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.
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.
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
)
}
)
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 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;
}
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 😃👋.