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 tips and tricks optimising JavaScript
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.
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
After
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 |
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 đđ.
Spread the word đ
Twitter Google+ LinkedIn