Optimización de imágenes
En esta página
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 sharpLos sitios que no optimizan imágenes no necesitan instalarla nunca.
Escribe un <img> normal
export default () => `
<html lang="es">
<body>
<img src="/images/hero.png" alt="Amanecer sobre el puerto">
</body>
</html>
`import { html, body, img } from 'javascript-to-html'
export default () =>
html({ lang: 'es' },
body(
img({ src: '/images/hero.png', alt: 'Amanecer sobre el puerto' }),
),
)export default function Home() {
return (
<html lang="es">
<body>
<img src="/images/hero.png" alt="Amanecer sobre el puerto" />
</body>
</html>
)
}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
- Redimensionado que nunca amplía — cada ancho configurado por debajo del de la fuente, y encima el ancho de la propia fuente. Una fuente de 750px con
widths: [400, 800, 1200]emite 400 y 750, y ahí se detiene. - Formatos modernos — con un solo formato obtienes un
<img srcset>normal; con dos o más se envuelve en un<picture>con una alternativa en el formato original. - Sin saltos de maquetación —
widthyheightse rellenan a partir de la imagen real, además deloading="lazy"ydecoding="async". - El mismo marcado en desarrollo —
sitelo(dev) reescribe las páginas y sirve las variantes bajo demanda, así que previsualizas lo que vas a publicar. - Una caché compartida — las variantes se indexan por hash de contenido en
node_modules/.sitelo/images, de modo que ni el desarrollo ni las recompilaciones codifican dos veces la misma imagen.
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/**'],
},
}widths— por defecto[400, 800, 1200]; el ancho de la propia fuente siempre corona la escaleraformats— por defecto['webp'];avif,webp,jpeg,pngquality— por defecto{ avif: 55, webp: 78, jpeg: 82 }; calidad del codificador por formato (png usa compresión, no calidad)sizes— el atributosizes; unsizespuesto en la etiqueta siempre ganadimensions— por defectotrue; añadewidth/heightlazy— por defectotrue; añadeloadingydecodingexclude— glob(s) o RegExp(s) de URLs de imagen que no hay que tocarassetsDir— por defecto'assets/img'cacheDir— por defecto'node_modules/.sitelo/images'remote— por defectofalse; optimiza imágeneshttps://en tiempo de compilaciónprune— por defectofalse; borra los originales que ya nada del build referenciadev— por defectotrue; ponlo afalsepara servir los originales sin tocar en desarrolloconcurrency— codificaciones en paralelo, por defecto CPUs − 1 (máx. 8); 1 cuando remote está activo
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">w— el ancho en píxeles. Vale cualquier ancho, no solo los dewidths; una fuente más pequeña conserva su propio tamaño en vez de ampliarse.h— el alto en píxeles. Solo, el ancho sigue la relación de aspecto; junto conwla imagen se escala hasta llenar esa caja y se recorta:?w=200&h=200es una miniatura cuadrada.fit— cómo se cumple una cajaw×h:cover(por defecto) la llena y recorta;containescala la imagen entera para que quepa dentro, sin recorte ni relleno;width/heightdicen lo que salió.background— rellena un resultadocontainhasta la caja exacta e implicafit=contain. Hex sin#(fff,1a1a1a,ffffff80), un nombre de color CSS otransparent, que necesita un formato con alfa; en jpeg sale negro.position— qué parte conserva un recortecover. Centrado por defecto; un borde o esquina (top,left,right-bottom, …), o las estrategias de sharp según el contenido,entropy/attention.format—avif,webp,jpegopng. Por sí solo conserva la escalera completa, en ese único formato.
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">entropyconserva la región con más detalle: bordes, textura, variación de color. El cielo plano, las paredes vacías y los fondos lisos se van primero. Bueno para paisajes, productos y todo aquello en lo que «cargado» significa «interesante».attentioninclina el recorte hacia el color saturado, la luminancia de alta frecuencia y los tonos de piel: una heurística de hacia dónde miraría una persona. Bueno para fotos de gente: tiende a encontrar caras y sujetos.
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.