After I published my last article, the result of Lighthouse dropped significatively, overall on mobile devices. Looking to the Lighthouse recommendations, one of the problem was that I was serving an image bigger than needed. It recommended to Serve Responsive Images. I didn't have idea about responsive images and how difficult and interesting is that topic.
An over-simplify definition is:
Different images in HTML that work well on devices with widely differing screen sizes, and resolutions.
There are many articles like: MDN Responsive Images and Responsive images done right guide picture srcset that give a detailed explanation about this topic.
HTML provides two main elements to render images:
According to this article: Responsive images, if you're just changing resolutions use srcset,
if we are just changing resolutions, the best is to use the srcset
attribute of the img
element.
Initially I tried to figure out what are the "optimal" dimensions of the responsive images, but there is not a perfect answer for that. After reading the articles: Responsive images 101 Part 9 Image breakpoints and Applying srcset, choosing the right sizes for responsive images at different breakpoints, I found that we should have more breakpoint at larger sizes. I have created these breakpoints for the moment:
After having the images for the different breakpoints, I still needed to solve the problem of show an explicit width
and height
to avoid the Cumulative Layout Shift (CLS) issue. This part was tricky. This article of Addy Osmani:
Optimize CLS
helped me a lot. Initially, I played with a 1.3 relation between the width and the height, but after read the previous
article I learned that it's very important all the images shared the same ratio. This site calculates the ratios:
Aspect Ratio Calculator. After have the images with the same ratio, I
needed to have a fix width
and height
of that ratio, but even after doing it, the browser always showed me the
images with the dimensions I set. The problem was that I needed to make the dimension dynamic using CSS!
This is the code of the img
element:
<img src="assets/img/originals/webcu-lighthouse.png"
srcset="assets/img/320x180/webcu-lighthouse.webp 320w, assets/img/640x360/webcu-lighthouse.webp 640w, assets/img/880x495/webcu-lighthouse.webp 880w, assets/img/1024x576/webcu-lighthouse.webp 1024w, assets/img/1200x675/webcu-lighthouse.webp 1200w, assets/img/1760x990/webcu-lighthouse.webp 1760w"
alt="Lighthouse results"
class="mb-2 w-full"
width="320"
height="180"
>
The width
and height
attributes specified have the same ratio that the images, and the TailwindCSS class w-full
makes sure the image expand to all the space available to it.
Resume: In order to add an explicit width and height to responsive images, we need to declare those attributes in the image with the same ratio that the images and adjust the width dynamically with CSS!
This article was very interesting as well: Setting height and width to images is important again. It's about the importance of declaring images with an explicit width and height.
Like we saw previously, the code of the img
element is large, tedious, and prone to error. Then I decided to extract
it to a Blade component:
// components/responsive-images.blade.php
@props(['imageName', 'imageExt', 'altText'])
@php
const DIMENSIONS = [
[320, 180],
[640, 360],
[880, 495],
[1024, 576],
[1200, 675],
[1760, 990],
];
$imageSet = [];
foreach(DIMENSIONS as $dimension) {
$imageSet[] = '/assets/img/' . $dimension[0] . 'x' . $dimension[1] . '/' . $imageName . '.webp ' . $dimension[0] . 'w';
}
$srcset = implode(', ', $imageSet);
@endphp
<img src="{{ '/assets/img/originals/' . $imageName . '.' . $imageExt }}"
srcset="{{ $srcset }}"
alt="{{ $altText }}"
class="mb-2 w-full"
width="320"
height="180"
>
We can use it now in any of our templates like this:
...
// Passing a variable to the component
<x-responsive-images
:imageName="$page->cover_image_name"
:imageExt="$page->cover_image_ext"
:altText="$page->cover_image_alt"
/>
/**
* Passing hardcode values to the component
* Notice the '' inside of the "". It's not a typo :)
*/
<x-responsive-images
:imageName="'Name-Image'"
:imageExt="'png'"
:altText="'Hello World of responsive images'"
/>
The attributes of a Blade component are directly accessible inside a php block
@props(['imageName', 'imageExt', 'altText'])
...
@php
...
$imageSet[] = ... $imageName . '.webp ' . $dimension[0] . 'w'
...
@endphp
The DIMENSIONS constant could be declared outside of the component.
I ordered my images like:
...
but it can be any other structure.
The resize process can be done in many ways, with a Lambda function in AWS for example. I have decided for the moment, given the actual requirements of the blog, to do it manually. To do it I'm using the library Sharp. It really has surprised me. Here is a copy of the script I'm using actually:
const fs = require("fs")
const sharp = require('sharp');
const dimensions = [
[320, 180],
[640, 360],
[880, 495],
[1024, 576],
[1200, 675],
[1760, 990],
];
const args = process.argv.slice(2)
const [image] = args;
const imageName = image.substring(0, image.indexOf('.'));
dimensions.forEach((dimension) => {
const [width, height] = dimension;
fs.mkdir(__dirname + `/source/assets/img/${width}x${height}`, () => {});
sharp(__dirname + '/source/assets/img/originals/' + image)
.resize(width, height, {fit: 'fill'})
.toFormat('webp')
.toFile(__dirname + `/source/assets/img/${width}x${height}/${imageName}.webp`)
;
})