Sustituir GIF por vídeo: los números del ahorro y la receta completa
Por qué un GIF pesa veinte veces lo que debería, cuánto se ahorra exactamente al convertirlo a vídeo, y el marcado y los comandos que reproducen el mismo comportamiento sin perder nada.
El GIF animado es un formato de 1989 diseñado para ilustraciones de 256 colores en pantallas de 320 por 200. Se usa hoy para clips de vídeo en color completo porque tiene una propiedad que ninguna alternativa tenía en su momento: se comporta como una imagen. Esa comodidad se paga con una relación de compresión entre diez y treinta veces peor que la de cualquier códec de vídeo moderno, y es probablemente el desperdicio de bytes más grande y más fácil de eliminar que queda en la web.
- Explicar por qué la compresión del GIF es estructuralmente incapaz de competir con un códec de vídeo.
- Cuantificar el ahorro con cifras medidas, no estimadas.
- Escribir el marcado que replica exactamente el comportamiento de un GIF con un
<video>. - Aplicar la conversión con comandos que producen ficheros listos para producción.
Por qué el GIF pierde por dos órdenes de magnitud
Dos limitaciones estructurales, ninguna solucionable.
No hay compresión temporal real. Un códec de vídeo comprime fundamentalmente prediciendo cada fotograma a partir de los anteriores: guarda un fotograma clave completo cada pocos segundos y, para el resto, guarda solo vectores de movimiento y diferencias. En un clip donde la cámara está quieta y solo se mueve una persona, el noventa por ciento del fotograma no se guarda. El GIF tiene un mecanismo primitivo de disposición de fotogramas que permite redibujar solo un rectángulo cambiado, pero no hay predicción de movimiento, no hay transformada de coseno, no hay cuantización perceptual. Cada fotograma es esencialmente una imagen comprimida con LZW, un algoritmo sin pérdida de 1984.
Solo 256 colores por fotograma. Un clip fotográfico tiene decenas de miles de colores distintos. Reducirlo a una paleta de 256 obliga a difuminar, y el difuminado introduce ruido de alta frecuencia. El ruido es exactamente lo que peor comprime cualquier algoritmo. Es decir: la limitación de color no solo empeora la calidad, además empeora el tamaño.
El resultado combinado es un formato que, para contenido en movimiento, produce ficheros de entre diez y treinta veces el tamaño del equivalente en vídeo, con peor calidad visual.
Los números
Medidas sobre un clip real de cinco segundos, 640 por 360 píxeles, 25 fotogramas por segundo, con movimiento moderado. Los tamaños de vídeo son con audio eliminado.
| Formato | Peso | Relación |
|---|---|---|
| GIF, paleta de 256, difuminado | 4,8 MB | 1x |
| GIF optimizado con paleta a medida | 3,1 MB | 0,65x |
| WebP animado con pérdida | 780 KB | 0,16x |
H.264, crf 26 |
310 KB | 0,065x |
AV1, crf 32 |
185 KB | 0,038x |
La lectura correcta de la última fila: el mismo clip, con mejor calidad visible, pesa el 3,8 por ciento del GIF. En un sitio con quince animaciones de este tipo, la conversión ahorra unos 69 MB de transferencia por visita completa. No hay ninguna otra optimización de esta guía con ese perfil de esfuerzo y resultado.
Y el ahorro no es solo de red. Un GIF de 4,8 MB hay que decodificarlo entero en memoria: 640 por 360 píxeles por 125 fotogramas a 4 bytes por píxel son 115 MB de mapa de bits sin comprimir si el navegador los mantiene descodificados, aunque en la práctica los navegadores descodifican bajo demanda y reciclan. En cualquier caso, la presión de memoria y el trabajo de decodificación del GIF ocurren en el hilo principal, mientras que un vídeo se decodifica en un hilo aparte y, con hardware, fuera de la CPU. Un listado con seis GIF animados es una máquina de producir tareas largas.
La receta completa
El marcado que replica el comportamiento de un GIF —arranca solo, en bucle, sin controles, sin sonido— es este:
<video autoplay loop muted playsinline
width="640" height="360"
poster="animacion-poster.avif"
aria-label="El cursor arrastra una tarjeta de la columna pendiente a la columna hecho">
<source src="animacion-av1.mp4" type='video/mp4; codecs="av01.0.05M.08"'>
<source src="animacion-h264.mp4" type='video/mp4; codecs="avc1.42E01E"'>
</video>
Cinco detalles que hay que entender y no copiar a ciegas.
muted es obligatorio, no opcional, porque sin él no hay reproducción automática. Además el fichero no debe llevar pista de audio en absoluto: un GIF no la tiene y arrastrarla añade bytes y complica la compatibilidad.
playsinline es obligatorio o iOS abrirá el clip a pantalla completa, comportamiento que ningún usuario espera de lo que parece una imagen.
width y height reservan el espacio exactamente igual que en una imagen, evitando el desplazamiento.
aria-label sustituye al alt que tendría el GIF. Un <video> no tiene alt, y sin etiqueta accesible el contenido desaparece para quien no lo ve. Describe lo que ocurre en la animación, no el hecho de que hay un vídeo.
El perfil avc1.42E01E es H.264 Baseline nivel 3.0, deliberadamente conservador porque estos clips son pequeños y la compatibilidad total vale más que el par de kilobytes que ahorraría el perfil High.
Los comandos de conversión, listos para usar:
# H.264 de compatibilidad. -an elimina el audio; el filtro garantiza dimensiones pares.
ffmpeg -i original.gif \
-movflags +faststart -pix_fmt yuv420p -an \
-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
-c:v libx264 -crf 26 -preset slow -profile:v baseline -level 3.0 \
animacion-h264.mp4
# AV1, mas pequeño, para navegadores que lo aceptan
ffmpeg -i original.gif \
-movflags +faststart -pix_fmt yuv420p -an \
-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
-c:v libsvtav1 -crf 32 -preset 6 \
animacion-av1.mp4
# Poster del primer fotograma
ffmpeg -i original.gif -vframes 1 -q:v 2 poster.png
El filtro de escalado con trunc resuelve un fallo que aparece siempre: muchos códecs exigen dimensiones pares y los GIF tienen anchos y altos arbitrarios. Sin ese filtro, ffmpeg falla con un mensaje poco claro sobre el formato de píxel.
La sustitución es casi perfecta, pero quedan tres asimetrías reales. Quien no las conoce descubre la primera en producción, con un informe de accesibilidad.
Uno: el vídeo no respeta la preferencia de movimiento reducido, y el GIF tampoco, pero del vídeo sí puedes hacerte cargo. Una animación en bucle infinito es exactamente el tipo de contenido que la preferencia existe para desactivar. La solución correcta no es quitar el vídeo, es no reproducirlo automáticamente y ofrecer el control:
<video loop muted playsinline width="640" height="360" poster="poster.avif"
aria-label="Reordenacion de tarjetas en el tablero">
<source src="animacion-av1.mp4" type='video/mp4; codecs="av01.0.05M.08"'>
<source src="animacion-h264.mp4" type='video/mp4; codecs="avc1.42E01E"'>
</video>const reducido = matchMedia('(prefers-reduced-motion: reduce)');
for (const v of document.querySelectorAll('video[loop][muted]')) {
if (reducido.matches) v.controls = true; // el usuario decide
else v.play().catch(() => { v.controls = true; });
}Fíjate en que el autoplay se ha quitado del HTML y la decisión se toma en JavaScript. Si el JavaScript no llega, el usuario ve el póster con controles, que es una degradación aceptable.
Dos: un GIF se puede pegar en un correo, en un chat y en un lector RSS; un <video> no. Si el contenido va a circular fuera de tu página, el vídeo no sirve. La solución intermedia es servir vídeo en la web y mantener el GIF como recurso descargable, no incrustarlo.
Tres: el primer fotograma de un GIF aparece en cuanto llegan sus primeros kilobytes; un vídeo no muestra nada hasta que tiene el fotograma clave completo. En una conexión mala, un GIF se dibuja progresivamente y un vídeo enseña el póster o un hueco. Por eso el póster no es opcional en esta sustitución: es lo que cubre la ventana entre la petición y el primer fotograma decodificado. Y por eso conviene que el póster sea el fotograma cero, como se explica en preload y poster.
Un apunte final sobre WebP y AVIF animados, que aparecen siempre en esta conversación: son mejores que el GIF por un factor de seis, pero peores que el vídeo por un factor de cuatro, y se decodifican en el hilo principal como cualquier imagen. Solo tienen sentido cuando de verdad necesitas que el recurso se comporte como una imagen: dentro de un <img>, en un background-image, o en un contexto donde no puedas usar <video>. Para todo lo demás, vídeo.
Encontrar los GIF que quedan
En un sitio grande no siempre sabes dónde están. Tres formas de barrerlo.
Desde el navegador, sobre una página cargada, el registro de recursos te da los que se han pedido de verdad, con su peso transferido:
performance.getEntriesByType('resource')
.filter(r => r.name.endsWith('.gif') && r.transferSize > 50_000)
.sort((a, b) => b.transferSize - a.transferSize)
.forEach(r => console.log((r.transferSize / 1024).toFixed(0) + ' KB', r.name));
Desde el repositorio, un barrido del directorio de recursos ordenado por tamaño localiza los candidatos antes de que lleguen a producción:
find ./public ./src -iname '*.gif' -size +100k -printf '%s\t%p\n' | sort -rn | head -30
Y en integración continua, la regla que cierra el asunto: fallar la compilación si aparece un GIF de más de 200 KB. Es una comprobación de dos líneas y evita que el problema vuelva a entrar por la puerta del gestor de contenidos.
Coge el GIF más pesado de tu sitio y conviértelo con los dos comandos de arriba. Anota el peso de los tres ficheros y calcula la relación. Después carga la página con los dos, GIF y vídeo, y compara en el panel de rendimiento con la CPU estrangulada a 4x: tiempo total del hilo principal durante los primeros cinco segundos, y memoria del proceso de renderizado. La diferencia de memoria suele sorprender más que la de bytes.