Mediciones de compresión de imágenes: diez muestras en el navegador
Diez muestras, cinco mediciones de cada una, un navegador. Son mediciones de codificador desde el mismo camino de trabajo que publica el sitio, incluidos los casos en los que el resultado no nos favorece.
¿Qué se midió y cómo?
Diez casos de siete archivos, con cinco mediciones cada uno: el benchmark mide el tiempo de compresión y el tamaño de salida. La fotografía de 12 MP se codifica a JPEG, WebP y AVIF, y la captura de 2 MP se codifica a PNG y WebP.
El arnés ejecuta el camino que se publica. Importa src/lib/pool.js y src/lib/worker.js — el mismo pool y el mismo worker que usa la aplicación para JPEG, PNG, WebP, AVIF y JXL. Para MotionPhoto importa src/lib/motion-photo/pipeline.js, que es el camino que toma la aplicación para ese formato; MotionPhoto tampoco pasa por el pool de workers en producción. No hay compresor exclusivo del benchmark ni una segunda implementación que pueda desviarse.
Cada caso se ejecuta una vez como calentamiento y luego cinco veces medidas. Los casos se ejecutan en secuencia, así que no compiten por el pool de cuatro workers. El tiempo reportado es la mediana de las cinco muestras. El p95 usa rango más cercano, que con cinco muestras es la ejecución más lenta: sin interpolación y sin fingir que cinco muestras describen la cola.
El navegador es Chrome 155 sin interfaz, con un perfil temporal nuevo en cada ejecución; las extensiones y la red en segundo plano están desactivadas. Se sirve desde localhost y el cronómetro empieza cuando el pool y los módulos WASM están listos. La ejecución no activa el aislamiento entre orígenes a propósito: eso haría que wasm-feature-detect informara hilos y cambiaría oxipng a su compilación con hilos, que no es el camino de producción.
La máquina es un Apple M4 con 10 núcleos y 16 GB de memoria, con macOS sobre arm64. El navegador informa 10 hilos de hardware; la aplicación limita su pool a cuatro workers.
¿Qué archivos se probaron?
Siete archivos, desde un JPEG de 0.5 MP hasta una fotografía de 24 MP, más un MotionPhoto de 12 MP.
| Archivo | Píxeles | Entrada | Qué representa |
|---|---|---|---|
| photo-12mp.jpg | 4000×3000 | 4.92 MB | Fotografía detallada |
| shot-2mp.png | 1920×1080 | 1.36 MB | Captura de interfaz con bordes nítidos y barras que imitan texto |
| flat-1mp.png | 1000×1000 | 118.1 KB | Gráfico plano y de bajo detalle |
| alpha-4mp.png | 2000×2000 | 8.26 MB | Imagen RGBA con canal alfa real |
| large-24mp.jpg | 6000×4000 | 9.66 MB | Fotografía de 24 MP |
| tiny-0.5mp.jpg | 800×600 | 219.6 KB | Imagen pequeña; aquí se nota el coste por llamada |
| motion-12mp.jpg | Fotograma de 4000×3000 + vídeo de 6 s a 1280×720 | 9.98 MB | MotionPhoto sintético, ensamblado con el escritor del repositorio |
Todas las muestras las genera ffmpeg a partir de fuentes integradas. El SHA-256 de cada archivo se registra en bench/fixtures/manifest.json y se comprueba antes de que el navegador lo cargue, de modo que cambiar una muestra es un fallo y no una línea base que se mueve en silencio.
¿Cuáles son los resultados?
Diez casos con ratios del 1.5% al 69.3% y medianas de 42.0 ms a 1,983.2 ms. La ratio es salida ÷ entrada; menos es más pequeño. La mediana y el p95 son tiempo de reloj de pared para el caso completo, incluido el viaje al worker. ms/MP divide la mediana entre los megapíxeles.
La tabla y los dos gráficos leen el mismo archivo, bench/published.json (id de ejecución, fecha, entorno); si discrepan, manda el archivo.
| Caso | Formato | MP | Entrada | Salida | Ratio | Mediana | p95 | ms/MP |
|---|---|---|---|---|---|---|---|---|
| photo-12mp-jpg | JPEG → JPEG | 12 | 4.92 MB | 444.4 KB | 8.8% | 677.0 | 733.4 | 56.4 |
| photo-12mp-webp | JPEG → WebP | 12 | 4.92 MB | 325.2 KB | 6.5% | 997.5 | 1021.7 | 83.1 |
| photo-12mp-avif | JPEG → AVIF | 12 | 4.92 MB | 121.8 KB | 2.4% | 1983.2 | 2026.8 | 165.3 |
| shot-2mp-png | PNG → PNG | 2.07 | 1.36 MB | 600.6 KB | 43.2% | 613.1 | 618.1 | 296.2 |
| shot-2mp-webp | PNG → WebP | 2.07 | 1.36 MB | 21.0 KB | 1.5% | 151.3 | 156.1 | 73.1 |
| flat-1mp-png | PNG → PNG | 1 | 118.1 KB | 61.0 KB | 51.7% | 516.2 | 562.9 | 516.2 |
| alpha-4mp-avif | PNG → AVIF | 4 | 8.26 MB | 370.1 KB | 4.4% | 1551.7 | 1626.3 | 387.9 |
| large-24mp-jpg | JPEG → JPEG | 24 | 9.66 MB | 846.1 KB | 8.6% | 1228.1 | 1231.0 | 51.2 |
| tiny-0.5mp-webp | JPEG → WebP | 0.48 | 219.6 KB | 20.4 KB | 9.3% | 42.0 | 43.4 | 87.5 |
| motion-12mp | MotionPhoto | 12 | 9.98 MB | 6.92 MB | 69.3% | 1334.0 | 1377.3 | 111.2 |
¿Qué dicen los números?
En la fotografía de 12 MP, AVIF es 3.65× más pequeño que JPEG y 2.93× más lento; WebP queda en medio, y PNG es la excepción sin pérdida con 296.2 ms/MP.
¿Cuánto más pequeño es AVIF que JPEG?
Cerca de un tercio de JPEG: 2.4% frente a 8.8% en la fotografía de 12 MP, es decir 121.8 KB frente a 444.4 KB. WebP produjo 325.2 KB. AVIF es 3.65× más pequeño que JPEG y 2.67× más pequeño que WebP. También es el más lento: 1,983.2 ms de mediana, 2.93× el tiempo de JPEG y 1.99× el de WebP. Por megapíxel, 165.3 ms frente a 56.4 de JPEG y 83.1 de WebP.
Ese es el intercambio en una línea: poco más de un cuarto de los bytes de JPEG a cambio de tres veces el tiempo de codificación. En un lote de cientos de imágenes el tiempo es real; en una sola imagen principal puede no importar.
¿Cuánto más pequeño y más lento es WebP que JPEG?
Un 26.8% más pequeño y 1.47× más lento: 325.2 KB frente a 444.4 KB, y 997.5 ms frente a 677.0 ms en la misma fuente de 12 MP. Frente a AVIF es 2.67× más grande y 1.99× más rápido. No es el mejor en ningún extremo; es el punto medio en ambas direcciones.
¿Qué tan rápido es JPEG?
677.0 ms para la fotografía de 12 MP (56.4 ms/MP) y 1,228.1 ms para la de 24 MP (51.2 ms/MP), el más rápido de los tres códecs aquí. El coste por megapíxel es casi plano, que es el aspecto de un códec de bloques rápido. Su salida es mayor que la de los dos formatos modernos, pero es el formato a batir en tiempo, y en esta máquina nada lo batió.
¿Por qué PNG es el más lento por megapíxel?
296.2 ms/MP en la captura de 2.07 MP, 4.05× los 73.1 ms/MP de WebP, porque la optimización sin pérdida tiene que buscar filtros y estrategias DEFLATE para cada píxel. La captura tardó 613.1 ms como PNG y 151.3 ms codificada a WebP. El PNG plano de 1 MP fue peor por píxel: 516.2 ms, porque el coste fijo del optimizador se reparte entre un solo megapíxel.
PNG es sin pérdida. Se conserva cada píxel, y oxipng pasa el tiempo buscando filtros y estrategias DEFLATE para un flujo más pequeño; la búsqueda es el objetivo y también el coste. Un códec con pérdida puede parar cuando alcanza la calidad objetivo. Un optimizador sin pérdida tiene que establecer que no queda nada por eliminar, y el número de combinaciones crece con la imagen.
La diferencia de tamaño es igual de contundente: la captura pesó 600.6 KB como PNG y 21.0 KB como WebP. No son salidas equivalentes —una es sin pérdida y la otra no—, pero explican por qué PNG es la elección correcta para un diagrama que vas a editar y la incorrecta para una fotografía que vas a publicar.
¿Un nivel de oxipng más alto siempre hace más pequeño el PNG?
No: el nivel 3 fue un 9.6–13.6% más pequeño en las tres capturas, pero ahorró solo cuatro bytes en el PNG fotográfico de 8.29 MP, con 8.2× el tiempo. El nivel de PNG se midió por separado con bench/png-levels.mjs, sobre las mismas muestras, con un calentamiento y cinco ejecuciones por nivel. En las tres muestras de captura (2.07, 8.29 y 15.73 MP), el nivel 3 tardó 7.8–8.5× más que el nivel 1 y produjo archivos un 9.6–13.6% más pequeños. En el PNG fotográfico de 8.29 MP produjo cuatro bytes menos que el nivel 1 —15,474,918 frente a 15,474,922— y tardó 8.2× más. En la imagen RGBA ahorró un 2.0% y tardó 7.1× más. La búsqueda extra no compra casi nada cuando la imagen ya es fotográfica.
Por eso el worker publicado usa ahora nivel 2 por defecto y baja a nivel 1 por encima de 4 MP. A 16 MP, el nivel 2 fue un 1.0% más grande que el nivel 1 y unas dos veces más lento, así que un nivel mayor no implica automáticamente un archivo menor. El coste del cambio se ve en los PNG planos de bajo detalle: la muestra plana de 1 MP pesa 62,501 B en nivel 2 frente a 48,138 B en nivel 3 —un 29.8% más— pero codifica en 437.9 ms en lugar de 2,028.3 ms, 4.6× más rápido. Las capturas pagan menos: a 2.07 MP el archivo de nivel 2 es un 7.2% más grande que el de nivel 3 y 4.1× más rápido. El sitio eligió el valor por defecto más rápido y acepta los bytes.
¿MotionPhoto comprime también el vídeo?
No: solo se recodifica el fotograma y el vídeo se copia byte a byte, así que la muestra sintética de 12 MP bajó un 30.7% mientras la mitad de ella no se tocó. Un MotionPhoto es un solo archivo: un fotograma JPEG con un vídeo añadido y, en algunos teléfonos, un gain map. El camino publicado decodifica y recodifica el fotograma, y luego reconstruye el contenedor a su alrededor. No se decodifica, no se recodifica y no se ofrece como destino. El gain map, cuando existe, también se copia, y su desplazamiento absoluto en el MPF se reapunta para que la visualización HDR siga funcionando.
Eso significa que la ratio del archivo completo está dominada por la proporción que ocupa el vídeo. En la muestra sintética de 12 MP, el fotograma son 5,156,418 B de los 10,463,084 B de entrada (49.3%) y el vídeo 5,306,666 B (50.7%); no hay gain map. El fotograma salió en 1,944,569 B, un 62.3% más pequeño, y el archivo completo pasó de 9.98 MB a 6.92 MB: un 30.7% menos. El ahorro del fotograma es grande; el del archivo está limitado por la mitad que nunca se tocó.
La comprobación con muestra real usa un MotionPhoto de un OnePlus 13: fotograma 7,140,978 B (44.7%), vídeo 7,968,544 B (49.9%), gain map 848,042 B (5.3%), total 15,957,564 B. Tras el camino de solo fotograma: fotograma 1,862,140 B (73.9% menos), vídeo y gain map idénticos byte a byte, total 10,712,466 B — de 15.96 MB a 10.71 MB, un 32.9% menos. El archivo reconstruido fue reconocido como MotionPhoto y se reprodujo en el teléfono. Esa es la ruta del gain map verificada en hardware real, no inferida de la muestra sintética.
¿Cuáles son las limitaciones?
Seis salvedades, empezando por las muestras sintéticas y una sola máquina.
- Las muestras las genera ffmpeg a partir de fuentes integradas. Son reproducibles, pero no son fotografías de cámara; una imagen distinta, con otro detalle, dará otra ratio.
- La muestra de MotionPhoto es sintética y no tiene gain map. Su ratio de archivo completo del 69.3% no es el 67.1% de la muestra real; la diferencia es el gain map ausente y el bitrate exacto del vídeo. La muestra real se reporta por separado.
- Estos números vienen de una máquina, un navegador y un tamaño de pool. Los tiempos absolutos cambiarán en otro hardware, y más en dispositivos móviles.
- Los números solo son comparables dentro del mismo entorno. La propia salida del benchmark lo dice: un resultado local darwin-arm64 y uno de CI ubuntu-latest son clases de runner distintas.
- Son mediciones de codificador, no una promesa sobre ningún teléfono concreto. No incluyen estado de la batería, limitación térmica, diferencias de versión del navegador ni el coste de decodificar el resultado.
- No se reporta la memoria máxima: medirla requiere aislamiento entre orígenes, que cambia el camino de oxipng. El benchmark informa "n/a" en lugar de un número de otra compilación.
¿Cómo se reproducen estos números?
Con cinco comandos, de npm ci a npm run bench.
npm ci
npm run build
npm run bench:fixtures
npm run bench:build
npm run bench
La ejecución escribe un JSON en bench/results/ e imprime la tabla anterior en stdout. La comparación de niveles PNG es npm run bench:png-levels. Las muestras se comprueban contra bench/fixtures/manifest.json antes de que el navegador las cargue; un archivo cambiado es un fallo duro, no una nueva línea base silenciosa.
Preguntas frecuentes
¿Qué formato comprime más?
AVIF: en la fotografía de 12 MP produjo 121.8 KB, un 2.4% de la entrada, frente al 6.5% de WebP y el 8.8% de JPEG.
¿Qué formato codifica más rápido?
JPEG: 677.0 ms para la fotografía de 12 MP, frente a 997.5 ms de WebP y 1,983.2 ms de AVIF.
¿Por qué PNG es más lento por megapíxel?
Porque es sin pérdida: oxipng busca filtros y estrategias DEFLATE para cada píxel. La captura de 2.07 MP tardó 296.2 ms/MP como PNG y 73.1 ms/MP como WebP, 4.05× menos.
¿MotionPhoto comprime el vídeo?
No. El vídeo se copia byte a byte y solo se recodifica el fotograma; en la muestra sintética de 12 MP el fotograma bajó un 62.3% y el archivo completo un 30.7%, de 9.98 MB a 6.92 MB.
¿Qué hace el nivel 2 frente al nivel 3 al tamaño del PNG?
El nivel 3 es más pequeño pero mucho más lento. El PNG plano de 1 MP pesa 62,501 B en nivel 2 frente a 48,138 B en nivel 3, un 29.8% más, pero codifica en 437.9 ms en lugar de 2,028.3 ms.
¿Cuántas ejecuciones se hacen por caso?
Un calentamiento y cinco ejecuciones medidas; la tabla reporta la mediana de esas cinco, y el p95 es la muestra más lenta.
Sobre este artículo
Publicado por tinyimg. El arnés, el generador de muestras y los archivos de resultados están en el repositorio, y el JSON sin procesar de esta ejecución se escribe en bench/results/. La firma es el proyecto, no una persona: la afirmación aquí no es autoridad, sino que el método está escrito y los números se pueden reproducir.