Why generate 12 versions of the same image when just 2 media-queries do the job? The users won’t notice. But Google will.
The responsive images that are in the wrong format, the images that are not compressed properly, and the images that are too big will all decrease the page speed and impact your SEO.
According to Google, anything above 2 seconds of download time will put off your users and discourage the crawlers from indexing your website.
I learnt it the hard way when I was rebuilding my website. My goal was to create a simple website that downloads in the blink of an eye. To do so, I went for Gatsby.js — it's fast and I know it.
After a few days of coding, the website was up and running. But - to my disappointment - when tested with PageSpeed Insights, it scored a modest 78/100 on mobile and a disastrous 57/100 on the desktop. Not good at all.
PageSpeed Insights suggested an easy fix to the problem. Just download the compressed images the tool has created and you'll be fine.
That sparked my curiosity. And the more I thought about sizes, formats and compression levels, the more I felt overwhelmed with the abundance of choices. PNGs, JPGs, SVGs, inline base64 encoded strings... or maybe WebPs?
To wrap my head around it, I dived into the world of pixels, waded through the muddy waters of random tips on the subject, and came up with a systematic approach.
And once I applied what I'd learnt, my PageSpeed rating increased from 78 to 91 on mobile and from 57 to 99 on the desktop!
In this post, I will show you how you can quickly generate responsive images that work in all browsers and massively reduce your download speeds.
If you want to optimise an image, you have to start with a high-quality image that has the right format and the right size:
The image that takes the full width of the 15” Macbook Pro screen is 1440 pixels wide but the actual resolution of the retina display is double that, at 2880x1800. Why? Because of its pixel density factor of 2.
Old monitors had a pixel density of 1. But since screen resolutions have increased in recent years, the hardware pixel is no longer equal to the software or CSS pixel. The relationship between hardware and CSS pixels is described by the following formula:
CSS Pixels = Hardware Pixels / Pixel density
Therefore, hardware resolution of 2880 pixels translates into 1440 CSS pixels on the retina display. This also explains why, when you inspect the full-width image in Developer Tools, you will see it as only 1440 pixels wide instead of the original 2880 pixels.
The retina display was a major breakthrough a few years ago. Today, mobile devices have even “denser” displays of 3 and even 4 for Samsung Galaxy S8+!
For the purpose of my experiment, I decided that to be razor-sharp, the full-width image should have the maximum width of 2880 pixels.
With the image height set to 600px and the quality - to 75%, Photoshop produced a massive 939KB file. That’s hardly acceptable.
After a few experiments with compression levels, it became clear that compressing JPGs below 60% quality results in a visible loss of quality. I set the quality to 60% as a starting point and the image size dropped to 681KB. Still, far from decent.
“WebP is a modern image format that provides superior lossless and lossy compression for images on the web.” according to Google.
After conversion to the WebP format, my image was not only smaller but also sharper! WebP shaved another 34% of the compressed JPEG size. "I'm on the right path", I thought!
Unfortunately, the WebP format is supported by Chrome and Opera, or just about 60% of all browsers, according to Can I use, so I knew I'd have to think of fallback options.
At last, the limits were set:
I also chose to support three breakpoints: 600px and 900px (here is why) and 2-pixel densities — 1x and 2x for retina displays. That meant 6 different images instead of just two. Supporting the WebP format doubled the number.
There are two primary ways to put an image on a website, either by using HTML’s img
element or a background-image
in CSS.
The basic HTML img
element has the src
attribute that points to the image URL:
<img src=“image.jpg” alt=“image description”/>
But you can go a step further and decide which image to serve depending on the screen’s pixel density by adding the srcset
attribute:
<img srcset=“image_1x.jpg 1x, image_2x.jpg 2x” src="image_1x.jpg"/>
Here, I've used two different screen densities: 1x and 2x and, depending on the actual display density, the browser will choose the right one. The src
attribute points to the fallback option.
At the moment, most browsers, except IE, Edge and Opera Mini, have the srcset
attribute implemented.
This solution seems like a step in the right direction. Unfortunately, your browser will always select the same image, with the same pixel density, regardless of the display size. And the same image will end up on both the desktop and the mobile device.
We need more control. And we can have it. Apart from pixel densities, the scrset
attribute accepts width units w
, an equivalent of CSS pixels.
The width unit enables the browser to choose the right image size for the given display capabilities. With two breakpoints (600px and 900px), we can go with three different image sizes:
<img
srcset=“image-sm.jpg 600w,
image-md.jpg 900w,
image-lg.jpg 1440w”
src="image_1x.jpg"/>
There is a caveat here. When the browser decides which image to fetch, it has no knowledge of our CSS! The CSS file hasn't been fetched at this point. And it assumes that the image will be displayed at the full width of the window.
If a full-width image is what you were after, then fine, but what if you wanted to place an image in a container that is only 50vw wide or smaller? Here comes the sizes
attribute into play. Let’s take a look:
<img
srcset=“image-sm.jpg 600w,
image-md.jpg 900w,
image-lg.jpg 1440w”
sizes="50vw"
src="image_1x.jpg"/>
By adding thesizes="50vw"
attribute, you are telling the browser that the image will be displayed at 50vw
, and based on this information, the browser will decide which image to display.
But what if you wanted to display your image at 50vw
on a big screen and at the full width of 100vw
on a mobile device? The sizes
attribute accepts also media queries!
You can specify that below the mobile breakpoint of 600px you want the browser to display your image at a full-screen width and for the width higher than mobile breakpoint - at 50vw
by adding the media query:
<img
srcset=“image-sm.jpg 600w,
image-md.jpg 900w,
image-lg.jpg 1440w”
size="(max-width: 600px) 100vw, 50vw"
src="image_1x.jpg" />
Remember that in the above line of code you are instructing the browser which image to choose because you know what the browser doesn't — the corresponding CSS. You still have to add the breakpoints in CSS explicitly.
This solution works really well but we are missing pixel densities here! If we stopped here, we would be sending the same image both to the displays with 1x pixel density and to the retina screens. Luckily, there is an easy fix to it.
Meet the HTML5 picture
element. It accepts the source
and img
elements as its children. We will use the source
element to list additional image formats that we want to serve to the browser.
But before we fix pixel densities, let's introduce smaller and sharper images in WebP format.
Let’s add the source
element as the first option inside the picture
element with your image in the WebP format followed by the img
pointing at the regular JPG image. Now, when the browser is not WebP-ready, it will gracefully fall back on to the img
element (eg. Safari).
<picture>
<source
srcset=“image.webp”
type=“image/webp” >
<img
src=“image.jpg”
type=“image/jpeg”
alt="image description">
</picture>
The source
element opens up a whole new world of possibilities. It accepts media queries!
First, in the media
attribute, we use the media query and then, in the srcset
attribute, we place the appropriate image. And we can use as many source
elements as we wish:
<picture>
<source
media="(min-width: 900px)"
srcset=“image-lg.webp”
type=“image/webp” >
<source
media="(min-width: 600px)"
srcset=“image-md.webp”
type=“image/webp” >
<source
srcset=“image-sm.webp”
type=“image/webp” >
<img
src=“image-lg.jpg”
type=“image/jpeg”
alt="image description">
</picture>
Above, we prepared three images in the WebP format, depending on the size of the display, and one JPG image as a fallback option.
The last secret of the srcset
attribute is that it also accepts pixel densities. We can decide which image we want to serve on which screen and at which pixel density. The trick is to list image files in the scrset
followed by a space and the pixel density factor, for example: 1x
, 2x
, 3x
or even 4x
.
<picture>
<source
media="(min-width: 900px)"
srcset=“image-lg_1x.webp 1x, image-lg_2x.webp 2x”
type=“image/webp” >
<source
media="(min-width: 601px)"
srcset=“image-md_1x.webp 1x, image-md_2x.webp 2x”
type=“image/webp” >
<source
media="(max-width: 600px)"
srcset=“image-sm_1x.webp 1x, image-sm_1x.webp 1x”
type=“image/webp” >
<img
src=“image-lg_1x.jpg”
type=“image/jpeg”
alt="image description">
</picture>
Since we sorted out the screen sizes and pixel densities for the WebP format, let’s have a closer look at the fallback option. In the end, some browsers don’t support the WebP format.
Here, we have to decide if we want to use the 1 or 2-pixel-dense images. Below, I went for the first option:
<picture>
<source
media="(min-width: 900px)"
srcset=“image-lg_1x.webp 1x, image-lg_2x.webp 2x”
type=“image/webp” >
<source
media="(min-width: 601px)"
srcset=“image-md_1x.webp 1x, image-md_2x.webp 2x”
type=“image/webp” >
<source
srcset=“image-sm_1x.webp 1x, image-sm_2x.webp 2x”
type=“image/webp” >
<img
srcset=“image-sm_1x.jpg 600w,
image-md_1x.jpg 900w,
image-lg_1x.jpg 1440w”
src=“image_lg_1x.jpg”
type=“image/jpeg”
alt="image description">
</picture>
We replaced the img
element with the picture
element. Where possible, we want to deliver images in the WebP format in three different sizes, depending on the display size, and 2 different pixel densities. If the browser doesn’t support the picture
element or the WebP format, it will fall back on to the standard img
element with three different sizes of JPGs.
Important: Notice that in the img
element the srcset
attribute should be placed before the src
attribute. Otherwise, the browser will download the src
image first and then, if it finds a better image in the srcset
, it will download this one as well. This way we would end up with two images instead of one.
We could go one step further and create another 3 source
elements for browsers that don’t support the WebP format and deliver JPG files instead.
Although it works great for Firefox, I've noticed that Safari will download both files, the JPG listed in the source
and the JPG from the img
element. Again, we would end up with two images instead of one.
If we don’t know the exact height and width of the container we want to cover with an image, we use generic elements like div
with the background-image
property pointing to the image URL:
background-image: url("/images/image.jpg");
CSS, similarly to HTML, enables image size optimisation.
The image-set
in CSS is the equivalent of the srcset
in HTML . At the moment, it is implemented in Chrome, Chrome for Android, Safari, iOS Safari and a few other browsers. There are polyfills that may be added to make the image-set
work on other browsers, but given that today Chrome and Safari combined are browsers of choice for 70% of users, there is a good chance that most browsers will implement the attribute in the near future.
But worry not, the regular background-image
as a fallback option will do the trick.
The structure is very similar to what we've just used in a srcset
attribute.
To create a full-width image element with a height of 500px, we have to start with the fallback option — the first background-image
in the code example below. Then, using the -webkit-image-set
, we need to list the WebP images for different pixel densities. And we have to repeat the process for different breakpoints using media queries.
One important thing to remember is that both Chrome and Safari use the WebKit layout engine but Safari doesn’t support the WebP format. That's why we have to add the last set of image-set
attributes with JPG images (it will be used by Safari even though it doesn’t start with -webkit
).
.bg-image {
width: 100vw;
height: 500px;
background-size: cover;
background-position: center;
background-image: url(/images/image-lg_1x.jpg);
background-image: -webkit-image-set(
url(/images/image-lg_1x.webp) 1x,
url(/images/image-lg_2x.webp) 2x
);
background-image: image-set(
url(/images/image-lg_1x.jpg) 1x,
url(/images/image-lg_2x.jpg) 2x
);
@media (max-width: 900px) {
background-image: url(/images/image-md_2x.jpg);
background-image: -webkit-image-set(
url(/images/image-md_1x.webp) 1x,
url(/images/image-md_2x.webp) 2x
);
background-image: image-set(
url(/images/image-md_1x.jpg) 1x,
url(/images/image-md_2x.jpg) 2x
);
}
@media (max-width: 600px) {
background-image: url(/images/image-sm_2x.jpg);
background-image: -webkit-image-set(
url(/images/image-sm_1x.webp) 1x,
url(/images/image-sm_2x.webp) 2x
);
background-image: image-set(
url(/images/image-sm_1x.jpg) 1x,
url(/images/image-sm_2x.jpg) 2x
);
}
}
Here, the background image is centered in the div
element and covers its whole area. Using the image-set
attribute, we are assigning two different images to two different pixel densities.
The fallback option with a standard url
takes care of the browsers that don’t support the image-set
attribute.
It’s very important to place the fallback option before the background-images
with the image-set
attribute. If you placed it after the image-set
attribute, for example, Safari would download both, the image from image-set
and the image from the fallback option if it found an image with a different file name.
The rest of the code follows the same pattern. Above, I added media queries for 600px and 900px breakpoints and a set of corresponding images in smaller sizes.
The fallback option always has to use the JPG format to avoid the situation where an image cannot be shown at all, that is when the browser doesn’t support neither the image-set
attribute nor the WebP format.
To improve user experience, we should not only compress and serve the smallest possible images, but we should also decrease the number of requests we send to the server.
The browser has to send a separate request for every single image. When sent to the server, the request has to first wait in a queue, which takes time. The more calls the browser makes, the longer the user has to wait.
That’s especially true when you have to download many small images. If possible, logos and icons should be saved as vector graphics (SVG). Small images can be embedded either in HTML or in CSS directly as base64 encoded strings.
Instead of passing a regular URL to the src
attribute in the img
element, we can pass the image as a string:
<img
src="data:image/png;base64,encoded string"
alt="img description" />
and in CSS:
.small-image {
background-image: url(data:image/png;base64,encoded string);
}
In most cases, the generated string will be around 30% bigger than the original image, but you will save time on another round trip to the server.
The most often raised argument against using base64 encoded images in CSS files is that images are non-blocking resources whereas CSS files are. It means that if you embed too many small images into your CSS, it will considerably increase the size of the CSS file and lengthen the time to the first paint of the website. That, in turn, will make the user wait longer before he or she can see any content.
Here you will find a great article on why you may consider dropping the idea of using encoded strings for images entirely.
The truth lays probably somewhere in the middle and injecting one or two small files as base64 strings into CSS or HTML shouldn’t do any harm.
At the end of this article, you will learn how to generate them. It can feel strange at first because these strings are thousands of characters long. Your .logo
class may look like this, but longer:
.logo {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAA
NSUhEUgAABqIAAAFvCAMAAAAWmCq0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZS
BJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAA
Dw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5U
Y3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0...);
}
Let's assume that you've just saved a perfect image and you'd like to create all the variations so you can use it on your website.
There are many tools that can help. From a simple compressjpeg.com, through compresspng.com or tinyjpg.com, to more advanced tools like ImageOptim for JPEG, PNGs and GIFs or ImageAlpha for PNGs.
You could also check imagecompressor, that gives a nice compression results (thanks Lisseth for sending the link).
On my quest to take full control of compression levels, formats and scaling, I needed a tool that would help me automate the whole process. And I didn't fancy drag-and-dropping dozens of images.
Both ImageMagic and GraphicsMagick are free and powerful pieces of software that painlessly pair with Grunt, the Javascript task runner.
Even better, there are Grunt plugins out there that simplify the task even further. Several quick tests showed that GraphicsMagick generates 20% smaller JPG images than ImageMagic at the same compression level. So the choice was clear.
Before we start to cut our way through the jungle of pixels, we have to prepare our tools and sharpen our axe. Download GraphicsMagick from here or use Homebrew
to install it.
brew install graphicsmagick
Next, install Grunt’s CLI globally:
npm install -g grunt-cli
Create a separate folder responsive-images
and init the project:
mkdir responsive-images
cd responsive-images
npm init
And finally, install the local version of Grunt:
npm install grunt --save-dev
Create two folders: src
for original images and dest
for the responsive images that Grunt and GraphicsMagick will generate:
mkdir src
mkdir dest
The original image should be saved at the resolution equal to or greater than the largest image you want to generate in the src/
folder. I saved mine as JPG at 100% quality and 2880 pixels wide. It was around 2.5MB.
First, we will generate responsive images using the grunt-responsive-images
plugin. Let’s install it:
npm install grunt-responsive-images --save-dev
Now, in the root directory of the project create an additional file: Gruntfile.js
:
touch Gruntfile.js
This is where we have to configure the plugin.
Copy and paste the code to the Gruntfile.js
and let me walk you through the code:
module.exports = function(grunt) {
grunt.initConfig({
responsive_images: {
dev: {
options: {
engine: "gm",
sizes: [
{ name: "sm", suffix: "_1x", quality: 60, width: 600 },
{ name: "sm", suffix: "_2x", quality: 60, width: 1200 },
{ name: "md", suffix: "_1x", quality: 60, width: 900 },
{ name: "md", suffix: "_2x", quality: 60, width: 1800 },
{ name: "lg", suffix: "_1x", quality: 60, width: 1440 },
{ name: "lg", suffix: "_2x", quality: 60, width: 2880 }
]
},
files: [
{
expand: true,
src: ["**/*.{jpg,png}"],
cwd: "src/",
dest: "dest/"
}
]
}
}
});
grunt.loadNpmTasks("grunt-responsive-images");
grunt.registerTask("default", ["responsive_images"]);
};
In options
, we'll set GraphicsMagick as our engine of choice: engine: “gm”
. You can also test ImageMagick by changing it to engine: “im"
.
Next, in the sizes
array, we have to specify the parameters of the images we want to produce, such as a name
that will be appended to the original name, a suffix
that will be added to the name as well, a quality
and a width
.
The resulting images will have the following naming structure:
original-[name]_[suffix}.jpg
For example, using the first sizes
object, Grunt will generate from the original my-image.jpg
the my-image-sm_1x.jpg
image at 60% compression level and 600 pixels wide.
Below the options, we need to list source and destination folders as well as patterns of file names that we want to process.
To enable the dynamic build of file objects, we'll set the expand
attribute to true
and define:
cwd
- source foldersrc
- an array of patterns to match. In our case, we want to match any folder (**
) inside the source folder and all files with extensions jpg
or png
dest
- destination folderThe above Grunt task will generate a set of JPG and/or PNG files, depending on the source image file extensions.
We also want to produce a corresponding set of WebP images.
We need another plugin to do the job: grunt-cwebp
. Let’s install it:
npm install grunt-cwebp --save-dev
Append the Gruntfile.js with the following configuration:
module.exports = function(grunt) {
grunt.initConfig({
responsive_images: {
...
},
cwebp: {
dynamic: {
options: {
q: 60
},
files: [
{
expand: true,
cwd: "dest/",
src: ["**/*.{jpg,png}"],
dest: "dest/"
}
]
}
}
});
grunt.loadNpmTasks("grunt-responsive-images");
grunt.loadNpmTasks("grunt-cwebp");
grunt.registerTask("default", ["responsive_images", "cwebp"]);
};
The grunt-cwebp
plugin will use the dest/
folder as the source of images. We want all the newly produced JPGs to have their WebP siblings and we will place them in the same folder.
Now, we can process the images:
grunt
For every image in the src/
folder, Grunt will generate 12 images in all the necessary sizes, pixel densities and in both JPG and WebP format!
If you ever wanted to generate base64 strings for inlining your images, here is how to do.
This time, we will use Grunt plugin: grunt-base64
.
Create a new project in a separate folder base64-images
. Init it with npm and install the local version of Grunt:
mkdir base64-images
cd base64-images
npm init
npm install grunt --save-dev
Install the grunt-base64
plugin:
npm install grunt-base64 --save-dev
In the root directory, create a new images/
folder and the Gruntfile.js
:
mkdir images
touch Gruntfile.js
and copy and paste the code into the Gruntfile.js:
module.exports = function(grunt) {
grunt.initConfig({
base64: {
dev: {
files: {
"images/output.b64": ["images/*.{jpg,png}"]
}
}
}
});
grunt.loadNpmTasks("grunt-base64");
grunt.registerTask("default", ["base64"]);
};
Place the small original image in the images/
folder and run Grunt:
grunt
After the task is finished, copy the whole content from the output.b64
file — that’s the base64 string that you can paste into the url
of the background-image
or into the src
attribute of the img
element.
There is also an easier way (on Mac OS X or Linux):
uuencode -m image-file-name remotename
The remotename
is not used and you can place even xyz
to get base64 string printed into the standard output — in most cases into the terminal window.
You have to use -m
option to get the base64 encoding.
Responsive images might feel overwhelming at first but with Grunt and image processing engines on your side, you can create a smooth process and automate most of the repetitive tasks. And I promise it's worth it. You will not only shine in PageSpeed Insights but you will also slash the time to the first paint of your website.
In my case, the original 939KB image shrank by 60% to 380KB (JPG) and by 77% to 218KB in the WebP format. In the end, my pixel-crusade paid off — the PageSpeed Insight rating for my website turned green. And it will work for you as well.
If you found this article helpful tweet to let me know how many pixels you managed to shave off and how it improved your website! And if you want to get an alert when my the next post is out, sign up below: