Todos los artículos

Componente Blade para imágenes adaptables

Creación de un componente Blade para facilitar el uso de imágenes adaptables.

Después de publicar mi último artículo, la puntuación de Lighthouse bajó considerablemente, sobre todo en dispositivos móviles. Al revisar sus recomendaciones, uno de los problemas era que servía una imagen más grande de lo necesario. Recomendaba servir imágenes adaptables. No sabía nada de ellas ni de lo difícil e interesante que es este tema.

Imágenes adaptables

Una definición muy simplificada sería:

Diferentes imágenes en HTML que funcionan bien en dispositivos con tamaños de pantalla y resoluciones muy distintos.

Hay muchos artículos, como Imágenes adaptables en MDN y Guía de imágenes adaptables con picture y srcset, que explican el tema en detalle.

¿Cómo mostrar imágenes adaptables en HTML?

HTML proporciona dos elementos principales para mostrar imágenes:

Según este artículo, Si solo cambias resoluciones, usa srcset, si únicamente cambiamos la resolución, lo mejor es usar el atributo srcset del elemento img.

Dimensiones «óptimas» de las imágenes adaptables

Al principio intenté determinar las dimensiones «óptimas», pero no hay una respuesta perfecta. Después de leer Imágenes adaptables 101: puntos de ruptura y Elegir tamaños adecuados con srcset para distintos puntos de ruptura, descubrí que deberíamos tener más puntos de ruptura para tamaños grandes. Por ahora he creado estos:

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

¿Qué ancho y alto debemos usar?

Después de preparar las imágenes para los distintos puntos de ruptura, aún tenía que especificar width y height explícitamente para evitar el desplazamiento acumulado del diseño (CLS). Esta parte fue complicada. El artículo de Addy Osmani Optimizar CLS me ayudó mucho. Inicialmente probé una relación de 1,3 entre ancho y alto, pero aprendí que es muy importante que todas las imágenes compartan la misma proporción. Este sitio calcula las proporciones: Calculadora de relación de aspecto. Una vez preparadas las imágenes con la misma proporción, necesitaba fijar width y height respetándola. Aun así, el navegador siempre mostraba las imágenes con las dimensiones que había especificado. ¡El problema era que necesitaba hacerlas dinámicas mediante CSS!

Este es el código del elemento img:

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

Los atributos width y height tienen la misma proporción que las imágenes, y la clase w-full de TailwindCSS hace que la imagen ocupe todo el espacio disponible.

Resumen: Para añadir un ancho y alto explícitos a imágenes adaptables, debemos declarar esos atributos con la misma proporción que las imágenes y ajustar el ancho dinámicamente mediante CSS.

También me resultó muy interesante Especificar el alto y el ancho de las imágenes vuelve a ser importante, sobre la importancia de declarar dimensiones explícitas.

Extraer las imágenes adaptables a un componente Blade

Como vimos, el código del elemento img es largo, tedioso y propenso a errores. Por eso decidí extraerlo a un componente Blade:

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

Ahora podemos utilizarlo en cualquiera de nuestras plantillas así:

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

Notas

  • Los atributos de un componente Blade son accesibles directamente dentro de un bloque PHP.

    @props(['imageName', 'imageExt', 'altText'])
    ...
    @php
    ...    
      $imageSet[] = ... $imageName . '.webp ' . $dimension[0] . 'w'
    ...
    @endphp
  • La constante DIMENSIONS podría declararse fuera del componente.

  • Organicé mis imágenes así:

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

    ...

    Pero se puede utilizar cualquier otra estructura.

Extra: script para redimensionar imágenes

El proceso de redimensionado puede hacerse de muchas maneras, por ejemplo con una función Lambda de AWS. Por ahora, dadas las necesidades actuales del blog, decidí hacerlo manualmente. Utilizo la biblioteca Sharp, que me ha sorprendido mucho. Esta es una copia del script que uso actualmente:

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

Referencias