Blade component for responsive images

Blade Responsive HTML Frontend

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.

Responsive images

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.

How to render responsive images in HTML?

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.

"Optimal" dimension of the responsive images

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:

  • 320x180
  • 640x360
  • 880x495
  • 1024x576
  • 1200x675
  • 1760x990

Which width and height should we use?

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.

Extracting the responsive images to a Blade component

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'"
/>

Notes

  • 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:

    • /assets/img/original
    • /assets/img/320x180

    ...

    but it can be any other structure.

Extra bonus: Resize Image script

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`)
    ;
})

References