Optimización de imágenes

Deja una imagen a tamaño completo en src/ o public/, apunta un <img> hacia ella, y sitelo la redimensiona, la convierte a un formato moderno y reescribe la etiqueta con un srcset. Nada que importar, ningún componente que aprender.

Activarlo

export default {
  images: true,
}

La codificación la hace sharp, una dependencia peer opcional: instálala junto a sitelo cuando actives las imágenes:

npm install -D sharp

Los sitios que no optimizan imágenes no necesitan instalarla nunca.

Escribe un <img> normal

import { html, body, img } from 'javascript-to-html'

export default () =>
  html({ lang: 'es' },
    body(
      img({ src: '/images/hero.png', alt: 'Amanecer sobre el puerto' }),
    ),
  )

Una imagen original de 3000×2000 sale así por el otro lado:

<img src="/assets/img/hero.a1b2c3d4-3000.webp"
     alt="Amanecer sobre el puerto"
     sizes="100vw"
     width="3000" height="2000"
     loading="lazy" decoding="async"
     srcset="/assets/img/hero.9f8e7d6c-400.webp 400w,
             /assets/img/hero.5b4a3c2d-800.webp 800w,
             /assets/img/hero.7c6d5e4f-1200.webp 1200w,
             /assets/img/hero.a1b2c3d4-3000.webp 3000w">

Una pantalla de hasta 400px de ancho recibe el archivo de 400, hasta 800 el de 800, hasta 1200 el de 1200, y cualquiera más ancha el de 3000 completo: el navegador elige el primer peldaño que cubre el viewport (el doble en una pantalla 2×).

Qué obtienes

La reescritura se ejecuta sobre el HTML ya compilado, así que cubre por igual las imágenes de src/ y de public/.

Una vez reescrita la etiqueta, nada apunta ya al original, así que por defecto se elimina del build. Solo se van los archivos realmente sin referencias: se recorre cada HTML, CSS, JS, XML y JSON del build, de modo que un original enlazado desde un <a href>, un og:image, un enclosure de RSS, un url() de CSS o una etiqueta data-no-optimize se queda. Las URL construidas en tiempo de ejecución en un script, o renderizadas por una isla de servidor, son las que no puede ver: pon prune: false si las tienes.

Opciones

export default {
  images: {
    widths: [400, 800, 1200],
    formats: ['avif', 'webp'],
    quality: { avif: 55, webp: 78, jpeg: 82 },
    exclude: ['**/og/**'],
  },
}

Dos formatos te dan <picture>

AVIF es más pequeño pero más reciente que WebP, así que listar ambos deja que el navegador elija y mantiene una alternativa para los antiguos:

<picture>
  <source type="image/avif" srcset="/assets/img/hero.*-400.avif 400w, ..." sizes="...">
  <source type="image/webp" srcset="/assets/img/hero.*-400.webp 400w, ..." sizes="...">
  <img src="/assets/img/hero.*-3000.png" alt="..." srcset="..." width="3000" height="2000">
</picture>

Un tamaño, por su nombre

Miniaturas de un carrusel, avatares, una cuadrícula de tarjetas: algunas imágenes solo se muestran en un tamaño, y una escalera entera se desperdicia en ellas. Los nombres de las variantes llevan un hash del contenido, así que no puedes escribirlos a mano; indica el ancho en la URL y la etiqueta sale con ese único archivo:

<img src="/images/hero.png?w=400" alt="Amanecer sobre el puerto">
<img src="/images/hero.png?w=400&format=jpeg" alt="Amanecer sobre el puerto">
<img src="/images/hero.png?w=200&h=200" alt="Amanecer sobre el puerto">
<img src="/images/hero.png?w=200&h=200&fit=contain" alt="Amanecer sobre el puerto">
<img src="/images/hero.png?w=200&h=200&background=fff" alt="Amanecer sobre el puerto">
<img src="/images/hero.png?w=200&h=200&position=top" alt="Amanecer sobre el puerto">
<img src="/assets/img/hero.9f8e7d6c-400.webp"
     alt="Amanecer sobre el puerto"
     width="400" height="267"
     loading="lazy" decoding="async">

Con varios formats configurados, un ancho fijo sigue dando un <picture>, con un archivo por <source>. Los nombres son los que usa vite-imagetools. Las URL remotas nunca se analizan (su query string pertenece al origen) y, con images desactivado, los hosts estáticos ignoran la query y sirven el original.

Dejar que la imagen elija el recorte

Un borde o una esquina es predecible, pero en un carrusel de fotos variadas el sujeto rara vez está dos veces en el mismo sitio. entropy y attention piden a sharp que mire cada imagen y desplace la ventana de recorte hacia donde encuentre algo que merezca conservarse:

<img src="/images/hero.png?w=300&h=300&position=entropy" alt="Amanecer sobre el puerto">
<img src="/images/team.jpg?w=300&h=300&position=attention" alt="El equipo">

Ambas son heurísticas simples, sin modelo ni entrenamiento, así que tómalas como un buen valor por defecto para muchas imágenes y no como una garantía para una. Para una imagen principal que te importe, indica el borde. La elección depende solo de la imagen, así que es determinista y se cachea como cualquier otra variante.

Cómo excluir imágenes

Las etiquetas que ya tienen un srcset, que están dentro de un <picture>, o que apuntan a un SVG, un GIF animado o una URL remota se dejan intactas. Para cualquier otra, dilo explícitamente:

<img src="/images/exact.png" alt="Pixel art" data-no-optimize>

Las imágenes de tarjetas sociales y favicons viven en <meta> y <link>, que esto nunca reescribe: conservan su URL fija.

Imágenes remotas

El contenido importado de un CMS suele apuntar al servidor de otra persona. Activa remote y esas imágenes se descargan, se optimizan y se sirven desde tu propio dominio:

export default {
  images: {
    remote: true,
  },
}

Una descarga que falla deja la etiqueta exactamente como estaba, con un aviso: un origen inestable nunca hace fallar tu compilación. Después, prune elimina los originales locales que ya no referencia nadie.