Las imágenes suelen ser la mayor parte del peso de una página y el elemento que más influye en el LCP (Largest Contentful Paint). En este tutorial montarás un pipeline en Ubuntu 24.04 que comprime los JPEG y PNG originales, genera versiones WebP y AVIF de cada imagen y lo repite automáticamente con un temporizador de systemd. Después configurarás Nginx para que entregue a cada navegador el formato más ligero que soporte, sin cambiar las URL de la aplicación.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Nginx instalado y sirviendo tu sitio en tudominio.com, con las imágenes en /var/www/tudominio.com/images. Ajusta esa ruta a la tuya en todos los pasos.
  • Una copia de seguridad de las imágenes: la compresión del paso 2 modifica los originales.

Paso 1: Instalar las herramientas de conversión

Usarás herramientas especializadas por formato, todas en los repositorios de Ubuntu:

  • jpegoptim: recomprime JPEG y elimina metadatos.
  • optipng: optimiza PNG sin pérdida.
  • cwebp (paquete webp): convierte a WebP.
  • avifenc (paquete libavif-bin): convierte a AVIF.
sudo apt update
sudo apt install jpegoptim optipng webp libavif-bin

Comprueba que están disponibles:

cwebp -version
avifenc --version | head -n 1

Cada comando debe mostrar el número de versión (libwebp 1.3.x y libavif 1.0.x en Ubuntu 24.04). Si alguno responde command not found, revisa que el paquete correspondiente se instaló sin errores.

Paso 2: Probar la conversión con una imagen

Antes de automatizar, prueba los ajustes con una imagen para comparar tamaños. Trabaja sobre una copia:

cd /tmp
cp /var/www/tudominio.com/images/foto.jpg .
jpegoptim --strip-all --max=85 foto.jpg
cwebp -quiet -q 80 foto.jpg -o foto.jpg.webp
avifenc -q 60 foto.jpg foto.jpg.avif
ls -lh foto.jpg*
-rw-r--r-- 1 usuario usuario 412K foto.jpg
-rw-r--r-- 1 usuario usuario  98K foto.jpg.avif
-rw-r--r-- 1 usuario usuario 186K foto.jpg.webp

--max=85 limita la calidad JPEG a 85 (solo recomprime si la imagen tiene más), -q 80 es la calidad WebP y -q 60 la calidad AVIF, ambas de 0 a 100. Abre los tres archivos y ajusta los valores si notas pérdida visible.

Las versiones se nombran añadiendo la extensión al nombre completo (foto.jpg.webp), lo que permite a Nginx encontrarlas a partir de la URL original.

Paso 3: Crear el script de optimización

El script recorre el directorio, procesa solo las imágenes nuevas o modificadas (las que no tienen versión WebP o la tienen más antigua que el original) y es seguro con nombres que contienen espacios. Créalo:

sudo nano /usr/local/bin/optimize-images
#!/usr/bin/env bash
set -euo pipefail

IMAGE_DIR="${1:-/var/www/tudominio.com/images}"
JPEG_MAX_QUALITY=85
WEBP_QUALITY=80
AVIF_QUALITY=60

find "$IMAGE_DIR" -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) -print0 |
while IFS= read -r -d '' img; do
    # Saltar si la versión WebP existe y es más reciente que el original
    if [[ -f "$img.webp" && "$img.webp" -nt "$img" ]]; then
        continue
    fi

    case "${img,,}" in
        *.png) optipng -quiet -o2 "$img" ;;
        *)     jpegoptim --quiet --strip-all --max="$JPEG_MAX_QUALITY" "$img" ;;
    esac

    cwebp -quiet -q "$WEBP_QUALITY" "$img" -o "$img.webp"
    avifenc -q "$AVIF_QUALITY" "$img" "$img.avif" > /dev/null

    echo "Optimizada: $img"
done

Hazlo ejecutable y asigna las imágenes al usuario con el que correrá el servicio (www-data), para que pueda escribir las nuevas versiones:

sudo chmod 0755 /usr/local/bin/optimize-images
sudo chown -R www-data:www-data /var/www/tudominio.com/images

Ejecútalo una vez a mano para procesar las imágenes existentes:

sudo -u www-data /usr/local/bin/optimize-images
Optimizada: /var/www/tudominio.com/images/foto.jpg
Optimizada: /var/www/tudominio.com/images/logo.png

Si lo ejecutas de nuevo, no debería mostrar nada: todas las imágenes están al día.

Paso 4: Automatizar con un temporizador de systemd

Un temporizador que lanza el script cada 15 minutos es más fiable que un bucle con inotifywait: si falla, systemd lo registra y lo vuelve a intentar en la siguiente ejecución. Crea el servicio:

sudo nano /etc/systemd/system/optimize-images.service
[Unit]
Description=Optimizar imágenes y generar WebP/AVIF

[Service]
Type=oneshot
User=www-data
Group=www-data
Nice=10
IOSchedulingClass=idle
ExecStart=/usr/local/bin/optimize-images /var/www/tudominio.com/images

Nice e IOSchedulingClass reducen la prioridad para que la conversión no compita con Nginx. Crea el temporizador:

sudo nano /etc/systemd/system/optimize-images.timer
[Unit]
Description=Ejecutar optimize-images cada 15 minutos

[Timer]
OnCalendar=*:0/15
Persistent=true

[Install]
WantedBy=timers.target

Actívalo y comprueba la próxima ejecución:

sudo systemctl daemon-reload
sudo systemctl enable --now optimize-images.timer
systemctl list-timers optimize-images.timer
NEXT                        LEFT       LAST PASSED UNIT                  ACTIVATES
Fri 2026-09-25 10:15:00 UTC 9min left  -    -      optimize-images.timer optimize-images.service

Para ver qué procesó en cada ejecución usa journalctl -u optimize-images.service.

Paso 5: Servir WebP y AVIF desde Nginx

Nginx elegirá el archivo según la cabecera Accept del navegador: AVIF si lo acepta y existe, si no WebP, y si no el original. Crea dos mapas en el contexto http:

sudo nano /etc/nginx/conf.d/image-formats.conf
map $http_accept $avif_suffix {
    default "";
    "~*image/avif" ".avif";
}

map $http_accept $webp_suffix {
    default "";
    "~*image/webp" ".webp";
}

Comprueba que el mime.types de Nginx conoce ambos formatos, para que se envíen con el Content-Type correcto:

grep -E 'avif|webp' /etc/nginx/mime.types

Si falta la línea de AVIF, edita /etc/nginx/mime.types y añade image/avif avif; dentro del bloque types, junto a la de image/webp.

Ahora abre el archivo de tu sitio, por ejemplo /etc/nginx/sites-available/tudominio.com, y añade este bloque dentro de server:

location ~* ^/images/.+\.(jpe?g|png)$ {
    add_header Vary Accept;
    add_header Cache-Control "public, max-age=2592000";
    try_files $uri$avif_suffix $uri$webp_suffix $uri =404;
}

Vary: Accept es imprescindible: indica a navegadores, proxies y CDN que la misma URL devuelve contenidos distintos según el cliente. Comprueba la sintaxis y recarga:

sudo nginx -t
sudo systemctl reload nginx

Paso 6: Verificar la negociación de formato

Simula tres navegadores con distintas cabeceras Accept:

URL=https://tudominio.com/images/foto.jpg
curl -sI -H "Accept: image/avif,image/webp,*/*" "$URL" | grep -iE 'content-type|content-length'
curl -sI -H "Accept: image/webp,*/*" "$URL" | grep -iE 'content-type|content-length'
curl -sI "$URL" | grep -iE 'content-type|content-length'
content-type: image/avif
content-length: 100352
content-type: image/webp
content-length: 190464
content-type: image/jpeg
content-length: 421888

En el navegador, abre las herramientas de desarrollo, pestaña Red, y comprueba la columna Tipo de las imágenes de la página.

Paso 7: Aplicar carga diferida y dimensiones en el HTML

El servidor no puede decidir qué imágenes están fuera de pantalla, así que este último ajuste va en las plantillas. Añade loading="lazy" a las imágenes que no se ven al cargar, e indica siempre width y height para evitar saltos de diseño (CLS):

<img src="/images/galeria-1.jpg" alt="Galería" width="1200" height="800" loading="lazy">

No lo uses en la imagen principal de la parte superior de la página: diferirla empeora el LCP.

Solución de problemas

El script falla con Permission denied. El usuario www-data no puede escribir en el directorio. Revisa el propietario con ls -ld /var/www/tudominio.com/images.

Nginx sigue enviando JPEG a un navegador moderno. Comprueba que existen foto.jpg.avif o foto.jpg.webp y que ningún otro location con expresión regular declarado antes captura las imágenes, porque Nginx usa el primero que coincide.

Una CDN delante entrega el formato equivocado. Asegúrate de que respeta la cabecera Vary: Accept o de que tiene activada la variación de caché por tipo de imagen; si no, purga la caché y revisa su configuración.

Conclusión

Tus imágenes se comprimen automáticamente al subirse y cada navegador recibe AVIF, WebP o el original según lo que soporte, con la misma URL y sin tocar la aplicación. Como siguientes pasos, puedes generar versiones redimensionadas para usar con srcset, activar HTTP/3 en Nginx y medir el impacto en el LCP con PageSpeed Insights.