Pipeline de Optimización de Imágenes para Servidores Web

Las imágenes representan típicamente el 60-70% del peso total de una página web, y su optimización es uno de los factores más determinantes para el rendimiento. Construir un pipeline de optimización de imágenes en Linux que genere automáticamente versiones WebP/AVIF, redimensione bajo demanda y se integre con una CDN puede reducir el tiempo de carga de las páginas a la mitad sin cambiar una línea del código de la aplicación.

Requisitos Previos

  • Servidor Linux (Ubuntu 20.04+, Debian 11+ o CentOS 8+)
  • Nginx instalado
  • Al menos 2 GB de RAM para el procesamiento de imágenes
  • Almacenamiento suficiente para las versiones optimizadas
  • Acceso root o sudo

Herramientas de Optimización

# Instalar el conjunto completo de herramientas de optimización
sudo apt update
sudo apt install -y \
    imagemagick \        # Procesamiento general de imágenes
    libvips-tools \      # Procesamiento de alta velocidad
    webp \              # Conversión a formato WebP
    jpegoptim \         # Optimización de JPEG sin pérdida
    optipng \           # Optimización de PNG
    gifsicle \          # Optimización de GIF
    pngquant            # Compresión de PNG con pérdida controlada

# Para formato AVIF (más reciente)
sudo apt install -y \
    libavif-bin \       # Herramientas AVIF (si está disponible)
    ffmpeg              # Conversión alternativa a AVIF

# Verificar instalaciones
vips --version
convert --version | head -1
cwebp -version

# Alternativa: instalar libvips desde el PPA para la versión más reciente
sudo add-apt-repository ppa:lovell/cgif -y
sudo apt update && sudo apt install -y libvips42

Conversión a WebP y AVIF

# Convertir una imagen JPEG a WebP
cwebp -q 80 imagen.jpg -o imagen.webp

# Convertir con calidad adaptativa (balance tamaño/calidad)
cwebp -q 75 -m 6 imagen.jpg -o imagen.webp
# -q: calidad (0-100), -m: método de compresión (0-6, más alto = mejor calidad)

# Convertir PNG a WebP sin pérdida
cwebp -lossless imagen.png -o imagen.webp

# Convertir directorio completo de JPEG a WebP con libvips (más rápido)
for img in /var/www/images/*.jpg; do
    vips copy "$img" "${img%.jpg}.webp[Q=80]"
done

# Convertir a AVIF usando ImageMagick (versión 7.1+)
convert imagen.jpg -quality 60 imagen.avif

# Conversión por lotes a AVIF con ffmpeg
ffmpeg -i imagen.jpg -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
  -c:v libaom-av1 -crf 30 -b:v 0 imagen.avif

Script de conversión automatizada:

cat > /usr/local/bin/optimize-images.sh << 'SCRIPT'
#!/bin/bash
# Script de optimización completa de imágenes
INPUT_DIR="${1:-/var/www/html/images}"
QUALITY_WEBP="${2:-80}"
QUALITY_AVIF="${3:-60}"

echo "Procesando imágenes en: $INPUT_DIR"

# Contador de progreso
TOTAL=$(find "$INPUT_DIR" -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" \) | wc -l)
PROCESSED=0

find "$INPUT_DIR" -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" \) | while read img; do
    base="${img%.*}"
    
    # Generar versión WebP si no existe o es más antigua que el original
    if [ ! -f "${base}.webp" ] || [ "$img" -nt "${base}.webp" ]; then
        cwebp -q "$QUALITY_WEBP" -m 4 "$img" -o "${base}.webp" 2>/dev/null
    fi
    
    # Optimizar el original sin pérdida de calidad
    case "${img##*.}" in
        jpg|jpeg)
            jpegoptim --strip-all --max=85 "$img" 2>/dev/null
            ;;
        png)
            optipng -o2 -quiet "$img" 2>/dev/null
            pngquant --force --quality=65-85 --skip-if-larger "$img" 2>/dev/null
            ;;
    esac
    
    PROCESSED=$((PROCESSED+1))
    echo "[$PROCESSED/$TOTAL] Procesado: $(basename $img)"
done

echo "Optimización completada."
SCRIPT

chmod +x /usr/local/bin/optimize-images.sh

Redimensionado Bajo Demanda con Nginx

Nginx puede redimensionar imágenes bajo demanda usando el módulo image_filter:

# Verificar que el módulo image_filter está disponible
nginx -V 2>&1 | grep image_filter
# Si no está, instalar nginx-full que lo incluye
sudo apt install -y nginx-full
# /etc/nginx/sites-available/images.conf
# Servidor de imágenes con redimensionado dinámico

# Caché de imágenes procesadas
proxy_cache_path /var/cache/nginx/images
    levels=1:2
    keys_zone=images_cache:10m
    max_size=5g
    inactive=7d;

server {
    listen 80;
    server_name images.tudominio.com;

    # Directorio raíz de imágenes originales
    root /var/www/images;

    # Servir WebP si el cliente lo soporta y existe el fichero
    location ~* \.(jpg|jpeg|png)$ {
        add_header Vary Accept;

        # Si el cliente acepta WebP y existe la versión WebP, servirla
        if ($http_accept ~* "webp") {
            set $webp_suffix ".webp";
        }

        try_files $uri$webp_suffix $uri =404;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Endpoint de redimensionado: /resize/ANCHO/ALTO/ruta/imagen.jpg
    location ~ ^/resize/(\d+)/(\d+)/(.+)$ {
        set $width $1;
        set $height $2;
        set $image_path $3;

        alias /var/www/images/$image_path;

        image_filter resize $width $height;
        image_filter_jpeg_quality 80;
        image_filter_buffer 10M;

        # Cachear el resultado
        proxy_cache images_cache;
        proxy_cache_valid 200 7d;

        expires 7d;
    }

    # Endpoint con solo ancho (mantiene proporción)
    location ~ ^/w/(\d+)/(.+)$ {
        set $width $1;
        set $image_path $2;

        alias /var/www/images/$image_path;

        image_filter resize $width -;
        image_filter_jpeg_quality 80;
        image_filter_buffer 10M;

        expires 7d;
    }
}
# Crear directorio de caché con los permisos correctos
sudo mkdir -p /var/cache/nginx/images
sudo chown -R www-data:www-data /var/cache/nginx/images

sudo nginx -t && sudo systemctl reload nginx

# Ejemplos de uso:
# Original: https://images.tudominio.com/foto.jpg
# 300x200:  https://images.tudominio.com/resize/300/200/foto.jpg
# Ancho 600: https://images.tudominio.com/w/600/foto.jpg

Lazy Loading

El lazy loading pospone la carga de imágenes fuera del viewport:

# Con HTML nativo (sin JavaScript - recomendado)
# Añadir loading="lazy" a todas las imágenes:
cat > /usr/local/bin/add-lazy-loading.sh << 'SCRIPT'
#!/bin/bash
# Añadir lazy loading a las imágenes en ficheros HTML
find /var/www/html -name "*.html" | while read file; do
    # Añadir loading="lazy" a <img> que no lo tengan
    sed -i 's/<img \([^>]*\)>\|<img \([^>]*\)\/>/& loading="lazy"/g' "$file"
    echo "Actualizado: $file"
done
SCRIPT

# Con Nginx, añadir el atributo via sub_filter
# (para aplicaciones dinámicas sin modificar el código fuente)
# Nginx: inyectar lazy loading en imágenes via sub_filter
location / {
    proxy_pass http://tu-aplicacion:3000;

    # Interceptar y modificar el HTML en tránsito
    sub_filter '<img ' '<img loading="lazy" ';
    sub_filter_once off;

    # Desactivar la compresión del proxy para que sub_filter funcione
    proxy_set_header Accept-Encoding "";
}

Integración con CDN

# Configurar Nginx para servir cabeceras adecuadas para CDN
cat >> /etc/nginx/sites-available/tudominio.conf << 'EOF'

# Cabeceras de caché para imágenes (las CDN las respetarán)
location ~* \.(jpg|jpeg|png|gif|ico|webp|avif|svg)$ {
    expires 1y;
    add_header Cache-Control "public, immutable, max-age=31536000";
    add_header Vary "Accept-Encoding, Accept";  # Importante para WebP
    
    # Permitir que la CDN cachee las respuestas
    add_header CDN-Cache-Control "max-age=604800";  # 7 días en CDN
    
    # Habilitar CORS para recursos de imágenes (si es necesario)
    add_header Access-Control-Allow-Origin "*";
    
    access_log off;  # No registrar peticiones de imágenes
}
EOF

# Script para pre-generar versiones WebP para una CDN
cat > /usr/local/bin/cdn-pregenerate.sh << 'SCRIPT'
#!/bin/bash
IMAGE_DIR="/var/www/html/images"
CDN_BUCKET="s3://tu-cdn-bucket/images"

# Generar y subir versiones WebP a S3/CDN
find "$IMAGE_DIR" -name "*.jpg" -o -name "*.png" | while read img; do
    webp_file="${img%.*}.webp"
    
    # Convertir si no existe
    if [ ! -f "$webp_file" ]; then
        cwebp -q 80 "$img" -o "$webp_file"
    fi
    
    # Subir a CDN (requiere aws-cli configurado)
    aws s3 cp "$webp_file" "$CDN_BUCKET/$(basename $webp_file)" \
      --content-type "image/webp" \
      --cache-control "max-age=31536000, public"
done
SCRIPT

Procesamiento por Lotes Automatizado

# Script completo de procesamiento con inotify (procesa imágenes al subirse)
sudo apt install -y inotify-tools

cat > /usr/local/bin/watch-images.sh << 'SCRIPT'
#!/bin/bash
WATCH_DIR="/var/www/html/uploads"

echo "Monitorizando: $WATCH_DIR"

inotifywait -m -r -e close_write --format '%w%f' "$WATCH_DIR" | while read FILE; do
    # Solo procesar imágenes
    case "${FILE##*.}" in
        jpg|jpeg|png|gif)
            echo "Nueva imagen detectada: $FILE"
            
            # Optimizar el original
            jpegoptim --strip-all --max=85 "$FILE" 2>/dev/null ||
            optipng -o2 -quiet "$FILE" 2>/dev/null
            
            # Generar versión WebP
            BASE="${FILE%.*}"
            cwebp -q 80 "$FILE" -o "${BASE}.webp" 2>/dev/null
            
            echo "Procesado: $FILE → ${BASE}.webp"
            ;;
    esac
done
SCRIPT

chmod +x /usr/local/bin/watch-images.sh

# Crear servicio systemd para el monitor
sudo tee /etc/systemd/system/image-watcher.service << 'EOF'
[Unit]
Description=Monitor y Optimizador de Imágenes
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/watch-images.sh
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl enable --now image-watcher

Solución de Problemas

Las imágenes WebP no se sirven a clientes compatibles:

# Verificar la cabecera Accept del cliente
curl -H "Accept: image/webp" -I https://tudominio.com/imagen.jpg \
  | grep -E "Content-Type|Vary"

# Asegurarse de que la configuración de Nginx incluye la comprobación de Accept
grep -n "webp" /etc/nginx/sites-available/tudominio.conf

El módulo image_filter consume demasiada memoria:

# Limitar el buffer de procesamiento
image_filter_buffer 5M;  # Reducir de 10M a 5M

# Añadir límite de tamaño de imagen para procesar
location ~ ^/resize/ {
    image_filter_buffer 5M;
    # Si la imagen es mayor de 5MB, devolver error 415
}

Imágenes AVIF no reconocidas por el servidor:

# Añadir el tipo MIME para AVIF en /etc/nginx/mime.types
types {
    image/avif  avif;
    image/webp  webp;
}

Conclusión

Un pipeline de optimización de imágenes bien configurado puede reducir el peso total de las páginas en un 40-70% mediante la generación automática de formatos modernos como WebP y AVIF, el redimensionado dinámico bajo demanda y el lazy loading nativo. La inversión en esta infraestructura mejora directamente las métricas de Core Web Vitals, especialmente el LCP (Largest Contentful Paint), con un impacto positivo tanto en la experiencia de usuario como en el posicionamiento en buscadores.