1 puntos por GN⁺ 2024-09-08 | 1 comentarios | Compartir por WhatsApp
  • Como GitHub Pages no soporta Brotli, este es un experimento para codificar HTML como una imagen WebP sin pérdida y restaurarlo con el decodificador de imágenes del navegador y JavaScript, reduciendo así la cantidad de datos transferidos
  • Un decodificador Brotli en WASM agrega un costo extra de 71 KiB~200 KB, y la Compression Streams API solo soporta gzip, deflate, deflate-raw, por lo que es difícil usarla como vía alternativa para decodificar Brotli
  • WebP sin pérdida VP8L puede producir resultados más pequeños que gzip incluso con datos de texto, gracias a la transformación predictiva, la reutilización de árboles Huffman por bloques de 16x16 y el color cache
  • El HTML de prueba de 439,478 bytes se redujo a 94,683 bytes con gzip y hasta 43,182 bytes con WebP; seguía siendo más grande que Brotli con 37 KiB, pero era alrededor de 2.2 veces más pequeño que gzip
  • Por el ruido anti-fingerprinting de Canvas 2D, los cambios en readPixels de WebGL, la pantalla vacía inicial y los problemas al restaurar el scroll, esto se parece más a un hack que expone las limitaciones del navegador que a algo aplicable en producción

El problema de no poder usar Brotli en GitHub Pages

  • Al reducir el tiempo de carga de una página, la compresión HTTP tiene más impacto que minificar el HTML
  • HTTP soporta gzip y Brotli mediante el encabezado Content-Encoding
    • gzip es barato de usar, así que suele venir activado por defecto
    • Brotli normalmente comprime mejor que gzip, pero es mucho más lento
  • GitHub Pages no soporta Brotli, así que el artículo más largo del sitio, Recovering garbled Bitcoin addresses, termina pesando 92 KiB con gzip en vez de los 37 KiB que podría tener con Brotli
  • Esa diferencia hace que el tiempo de carga aumente innecesariamente en 2.5 veces

La primera alternativa pensada y dónde se atoró

  • Si GitHub permitiera subir y servir archivos Brotli precomprimidos, el problema podría evitarse, pero esa función no existe
  • La opción de descomprimir directamente en el cliente con JavaScript pierde ventaja por el tamaño del decodificador WASM
    • brotli-dec-wasm pesa alrededor de 200 KB
    • tiny-brotli-dec-wasm pesa 71 KiB
    • La comparación termina siendo entre gzip 92 KiB y un cuerpo Brotli de 37 KiB + 71 KiB, así que el beneficio desaparece
  • El stack HTTP del navegador sí tiene un decodificador Brotli, pero DecompressionStream de la Compression Streams API solo acepta gzip, deflate, deflate-raw
  • Incluso precomprimiendo gzip con Zopfli, queda en 86 KiB, así que sigue siendo más grande que Brotli

Usar un formato de imagen como contenedor de compresión

  • Como el navegador ya puede decodificar imágenes, se pueden meter los datos en los píxeles de una imagen y volver a leerlos con la Canvas API, sin agregar una nueva lógica de descompresión
  • GIF acomoda los datos en orden row-major y luego aplica LZW, pero DEFLATE de gzip fue diseñado justamente para reemplazar LZW, así que es difícil esperar una ganancia
  • PNG usa DEFLATE, pero antes aplica una transformación predictiva que comprime no los píxeles originales, sino sus diferencias con los píxeles vecinos
    • Ejemplo: comprimir [a, b, c, d] en lugar de [a, b-a, c-b, d-c]
    • Cuanto menor sea la diferencia entre el valor predicho y el real, más favorable resulta para la compresión Huffman
  • El núcleo del experimento es usar VP8L, la variante sin pérdida de WebP, para comprimir datos de bytes genéricos

En qué se diferencia VP8L de gzip

  • WebP tiene variantes con pérdida y sin pérdida; aquí solo se trata el formato sin pérdida VP8L
  • VP8L usa transformación predictiva como PNG, pero en vez de DEFLATE usa un esquema parecido a DEFLATE creado por Google
  • DEFLATE puede dividir un archivo en varios fragmentos y usar un árbol Huffman adecuado para cada uno
    • Si en un mismo HTML se mezclan JavaScript, SVG y marcado, pueden convenir árboles distintos
  • VP8L puede definir una tabla de árboles Huffman arbitrariamente grande y usar un árbol distinto para cada bloque de 16x16 píxeles
    • Si después de JavaScript viene CSS y luego otra vez JavaScript, DEFLATE puede terminar codificando varias veces árboles parecidos
    • VP8L puede reutilizar árboles, así que puede cambiarlos con más frecuencia y a menor costo
  • El color cache de VP8L permite representar valores de forma corta, como “copiar un píxel reciente con ciertas propiedades”

Primer experimento de compresión con WebP

  • El archivo de prueba fue el HTML de Recovering garbled Bitcoin addresses
    • Tamaño original: 439,478 bytes
    • gzip --best: 94,683 bytes
  • Con el crate webp de Rust, los bytes se convirtieron en una imagen RGB en escala de grises y luego se comprimieron como WebP sin pérdida
  • Se usó escala de grises por la transformación subtract green de WebP
    • En escala de grises, si se resta el canal G de R/B, R/B quedan efectivamente en 0
    • WebP codifica los tres canales con árboles Huffman separados, así que un canal de valor fijo ocupa casi espacio O(1)
  • Al principio se intentó crear una imagen 1xN, pero WebP solo soporta hasta 16383x16383, así que apareció el error VP8_ENC_ERROR_BAD_DIMENSION
  • Al ajustarlo a un formato 16383xN, el resultado quedó en 45,604 bytes, 2 veces más pequeño que gzip y más pequeño incluso que bzip2 con 49,764 bytes

Ajustes específicos para WebP

  • Si se usa orden row-major en una imagen ancha, dentro de un bloque de 16x16 se mezclan bytes que en la entrada original estaban muy alejados
  • Al cambiar la forma de la imagen a una más alta y angosta, 27x16383, el resultado de compresión bajó a 43,232 bytes
  • Se comparó la opción de rendimiento de compresión method de cwebp entre 0~6
    • method 0: 48,902
    • method 1: 43,546
    • method 2: 43,442
    • method 3: 43,292
    • method 4: 43,232
    • method 5: 43,182
    • method 6: 43,182
  • Se eligió method 5 porque daba el mismo tamaño que method 6, pero era más rápido
  • En ese estado, WebP era 2.2 veces más pequeño que gzip y 1.2 veces más grande que Brotli

Benchmark con varios archivos

  • Los datos de comparación fueron snappy testdata, Canterbury Corpus y Large Corpus y 2 archivos SVG
  • Los formatos comparados fueron gzip --best, brotli --best, bzip2 --best y el script de compresión con WebP
  • Salvo en archivos muy pequeños como grammar.lsp, xargs.1 y unas pocas excepciones, WebP casi siempre superó a gzip
  • Las excepciones fueron kennedy.xls y paper-100k.pdf
    • paper-100k.pdf tiene 19 KB de XML y luego datos comprimidos, así que en la práctica se estaba midiendo una porción pequeña de datos
    • kennedy.xls también muestra un rendimiento extraño frente a Brotli y bzip2, y puede ser un archivo con muchos datos heterogéneos cercanos entre sí, difícil para el compresor
  • WebP tendía a rendir un poco peor que bzip2, aunque en algunos casos lo superaba
  • WebP siempre fue peor que Brotli, salvo excepciones como fireworks.jpeg, que se parece más a un blob aleatorio casi uniforme
  • En datos grandes de texto plano sí mostró mejoras medibles frente a gzip
    • También hubo mejoras en los archivos SVG
    • En html_x_4, WebP logró una tasa de compresión de 3.3%; peor que Brotli con 2.8%, pero muy superior a gzip con 13%

Restaurarlo con JavaScript

  • La decodificación de WebP se puede implementar con fetch, createImageBitmap, OffscreenCanvas, getImageData y TextDecoder
  • La idea es usar el canal R de los píxeles como bytes del HTML original, decodificarlos como UTF-8 y ponerlos en document.documentElement.innerHTML
  • La Canvas API se usa con frecuencia para fingerprinting, así que algunos navegadores agregan ruido al resultado de getImageData
    • En Firefox con strict tracking protection, menos del 1% de los píxeles puede verse afectado
    • En el HTML, ese ruido aparece como si fueran errores tipográficos
  • Usando readPixels de WebGL, en ese momento funcionaba sin ruido
    • WebGL solo soporta texturas de hasta 2048x2048 de forma confiable, así que hubo que volver a ajustar el límite de tamaño
    • Este código de restauración, tras minificarlo, pesaba cerca de 550 bytes
  • Sumando WebP y el código, el total quedaba en 44 KiB, frente a gzip con 92 KiB y Brotli con 37 KiB
  • Después del 15 de abril de 2026, Firefox añadió protección anti-fingerprinting también a readPixels, así que este método ya no funciona tal cual

Parpadeo de pantalla y problema del scroll

  • Como await se resuelve sobre promesas, el navegador considera que la ejecución del script terminó antes de que termine la descarga del WebP
  • Como el DOM sigue vacío, el usuario ve por un momento una pantalla blanca vacía
  • Como mitigación, se pueden dejar en el HTML con gzip los estilos y unos 8 KiB de la parte superior de la página, y comprimir con WebP solo el contenido bajo el viewport
  • Restaurar la posición del scroll al recargar también causa problemas
    • Por ejemplo, si se recarga en la posición Y = 5000px pero la altura de la página es 0px, la posición se reinicia
    • Agregar un div temporal muy grande ayuda
  • En vez de document.write, hay que asignar a document.documentElement.innerHTML para actualizar el documento actual sin reemplazarlo por uno nuevo

Meter WebP directamente dentro de JavaScript

  • Para reducir un poco más la latencia, WebP se puede incrustar directamente dentro de JavaScript
  • La forma más simple es usar un data URL en base64
  • base64 aumenta el tamaño original en 1.33 veces, pero gzip compensa casi por completo ese aumento
    • Si compressed.webp se convierte a base64, queda en 57,576 bytes
    • Si luego se comprime con gzip --best, queda en 43,519 bytes
  • Un blob comprimido como WebP se parece a datos aleatorios casi uniformes, y el paso de 8 bits a 6 bits de base64 hace que el árbol Huffman de gzip actúe casi como una transformación inversa
  • También se podría usar Unicode y UTF-16, pero base64 sigue siendo una primera solución suficientemente buena

Aplicación real y estado posterior

  • Al momento de escribirse el texto, esta misma página estaba comprimida con WebP a partir de la sección “Fool me twice”, salvo en navegadores viejos o con JavaScript desactivado
  • La imagen WebP de la página real era alta y angosta, aunque también se mostraba un ejemplo cuadrado de WebP por estética
  • En la imagen, la parte superior e inferior claras correspondían al texto y al código; la zona rayada cerca de 1/5 de la altura era un diagrama, y la mayor parte de las zonas oscuras correspondían al texto dentro del diagrama
  • La reducción real fue limitada
    • Página original con gzip: 88 KiB
    • Página con WebP aplicado y gzip: 83 KiB
    • Estimación con Brotli: 69 KiB
  • Después del 15 de abril de 2026, para que los visitantes de Firefox no vieran contenido roto, la página volvió a una versión sin esta compresión
  • El código en Rust, el corpus y otros archivos están publicados en GitHub

1 comentarios

 
GN⁺ 2024-09-08
Opiniones de Hacker News
  • Si ignoras la latencia, sí, pero en la práctica parece que el tiempo de carga aumenta alrededor de 0.001%
    El aumento de tamaño no es significativo comparado con la latencia de ida y vuelta, y puede que el tiempo que toma descomprimir sea mayor que el tiempo ahorrado por transmitir 55 KiB menos
    Aunque es un experimento interesante, en este caso es muy probable que la experiencia de usuario empeore; la velocidad sería casi la misma y solo se perdería compatibilidad

    • Si solo optimizas el tiempo de carga y asumes la velocidad de datos de todo el mundo, tienes razón, pero muchas veces no quiero que quienes crean sitios web o apps decidan con tanta facilidad por mí el compromiso entre velocidad y datos
      El problema es que hay gente como el autor de TFA que se esfuerza por reducir 100 KB a 50 KB, pero también hay sitios que, sin pensarlo, me envían decenas de MB en imágenes cuando solo quiero ver el horario de un restaurante usando datos en roaming
      La conciencia sobre los recursos existe, pero por desgracia está distribuida de forma muy desigual
    • No es solo cuestión del tiempo de descompresión. Solo puedes descomprimir después de descargarlo todo, mientras que el navegador puede descomprimir y renderizar de inmediato el HTML que se transmite desde el servidor
      Si se corta la conexión, lo pierdes todo, y ni siquiera puedes leer la parte descargada
      En conexiones normales la diferencia no es significativa, y en conexiones tan lentas o inestables como para que 50 KB importen, este método definitivamente empeora las cosas. Es un experimento interesante, pero preferiría que no lo aplicaran a sitios
    • Si no está en caché, hay un archivo Symbols-2048-em%20Nerd%20Font%20Complete.woff2 de 850K, así que eso prácticamente tapa la diferencia
    • Esa diferencia de tamaño es lo bastante grande como para afectar la cantidad de viajes de ida y vuelta necesarios. Con un valor moderno razonable de ventana inicial de congestión, debería ahorrar aproximadamente un viaje de ida y vuelta
      No será una diferencia de 2.5 veces, pero tampoco es 0.001%
    • Si lo que ahorras es menos que una ventana de recepción TCP, no habrá diferencia en la latencia
      En redes con pérdida podría notarse, pero no estoy seguro
  • No sé por qué readPixels no es objetivo de las protecciones contra fingerprinting. Como no esparce erratas casi invisibles por toda la página, para mí está bien
    La parte que dice que en el HTML comprimido con gzip solo quedan los estilos y unos 8 KiB de la parte superior de la página, y que solo el contenido debajo del viewport se comprime con WebP, explica por qué el texto se cortaba de golpe después de una frase cualquiera y seguía una página en blanco
    Uso LibreWolf, así que WebGL está desactivado, y para juegos web aleatorios que necesitan WebGL uso Chromium. Al activar WebGL, el artículo funcionó bien y, sinceramente, es una técnica bastante limpia

    • Si no funciona en todos los navegadores web modernos, incluso con protección contra fingerprinting activada, y tampoco tiene fallback para navegadores antiguos, es difícil llamarla limpia
      La WWW debería ser universalmente accesible partiendo de HTML común y corriente y mejorando progresivamente
  • También es posible usar Brotli directamente en el navegador web, aunque por supuesto tiene limitaciones
    Creo que una propuesta de JS1024 de 2022 [1] fue la primera demostración de este concepto, y también hay código de prueba de concepto para compresión arbitraria. Lamentablemente, no servía para su propósito original, que era code golf de tamaño
    La limitación clave es que en la práctica está restringido a caracteres ASCII y, por razones obvias, es muy sensible al stack de renderizado. Ahora mismo parece que no funciona en Firefox

    [1] https://js1024.fun/demos/2022/18/readme

    [2] https://gist.github.com/lifthrasiir/1c7f9c5a421ad39c1af19a9c...

    • La clave para entender este enfoque es esta parte, sin necesidad de entrar hasta el fondo en cómo lograron encajarlo
      Solo se puede usar el formato de archivo de fuentes WOFF2, para el que Brotli fue diseñado originalmente, pero para aprovecharlo hay que crear un archivo de fuente completo
      Los navegadores recientes suelen limpiar las fuentes con OpenType Sanitizer (OTS), porque poner directamente archivos de fuentes no confiables en el sistema es muy peligroso; por eso hay que crear un archivo WOFF2 que sea lo bastante normal como para que OTS lo acepte, pero que aun así contenga y permita extraer la secuencia de bytes deseada
      Tras muchos fracasos, la idea de terminar usando los anchos de glifo, es decir, los avances, que se codifican como una secuencia de enteros con signo de 2 bytes casi sin restricciones, es fantástica
    • Corrijo: todavía funciona en Firefox. Solo había olvidado que en Firefox el nivel de zoom debe estar exactamente en 100%
    • Esta técnica es realmente sorprendente y mucho más genial que mi artículo. Mis respetos
  • Chromium lo estuvo bloqueando durante mucho tiempo, pero zstd ya está llegando a la web. Por fin llegó a Chrome, así que ahora solo falta que Safari se ponga al día

    • Me gustaría que todo pasara a Zstandard, pero en este caso específico, hasta donde sé, Brotli y Zstandard son casi iguales cuando el uso de memoria del descompresor es el mismo
    • Al menos parece estar en la lista de pendientes: https://webkit.org/standards-positions/#position-168
  • Estoy creando Batch Compress (https://batchcompress.com/en) y, poco después de agregar soporte para WebP, lo cambié a la opción predeterminada.
    Hasta donde sé, ya estaba generando los JPEG más pequeños entre las herramientas de compresión web, pero WebP terminaba pesando apenas alrededor del 50% de un JPEG. Tras agregar el soporte, pasarlo a predeterminado fue una decisión fácil.
    Como el sitio tiene bastantes usuarios, pensé que habría algunas quejas después de cambiar WebP a predeterminado, pero ya pasó más o menos un mes y solo hubo una consulta o queja relacionada con WebP.
    Parece que ahora casi todas las herramientas y navegadores soportan WebP. Hace poco vi un sitio web donde no se manejaban bien las subidas de imágenes WebP y eso bloqueaba el siguiente paso, pero hoy en día casi todo lo soporta bien.

    • Si WebP reduce el tamaño de archivo en 15~20% o más frente a JPEG, ese ahorro viene de una pérdida de calidad, no de una mejora en la compresión.
      Si se comprime y optimiza bien un JPEG, no debería quedar muy por detrás de WebP.
      Siempre se puede crear un WebP que se vea casi igual que un JPEG y reducir el tamaño de archivo, pero lo mismo pasa si se vuelve a comprimir como un JPEG que se vea casi igual.
      Es una característica de todos los códecs con pérdida y, como el tamaño de archivo crece exponencialmente a medida que sube la calidad, a la gente siempre le sorprende cuánto puede cambiar el tamaño con una degradación de calidad muy pequeña, casi imperceptible.
    • Me da curiosidad bajo qué métrica de comparación de calidad se dice que WebP pesa alrededor del 50% de un JPEG.
      WebP tenía mala fama por sus valores predeterminados terribles, que arruinaban los detalles en las zonas oscuras.
  • Revisando el código fuente, vi que a la declaración doctype le falta un espacio. La forma actual es incorrecta; debería llevar un espacio.

  • Probé este truco hace tiempo. Curiosamente no recuerdo para qué lo usé, pero probablemente era para ver si se podía hacer, y también dejé un comentario aquí: https://gist.github.com/gasman/2560551?permalink_comment_id=...
    También encontré un prototipo antiguo, que parece haber sido solo una prueba: https://retr0.id/stuff/bee_movie.webp.html

    • Esa página rompe mi extensión de gestos del mouse.
      Hoy se les dice extensiones, no add-ons, a esas cosas que inyectan scripts, pero en fin: me parece interesante el enfoque de pasar “basura” al principio y luego pegar JS detrás para devolvérselo a la página.
      El fanático de seguridad que llevo dentro se pregunta si esto podría habilitar un ataque cuando hay datos proporcionados por usuarios, como un formulario de comentarios.
      Me pregunto si alguien podría encontrar una secuencia de bytes para poner en un comentario que, después de la compresión, se convierta en una etiqueta script ubicada antes de mi script y termine ejecutándose.
    • Según mi experiencia, WebP no encajaba bien con el caso general en el que esta técnica sería realmente útil, es decir, datos de menos de 10 KB.
      La mayor parte de lo que WebP lossless aporta sobre PNG tiene que ver con el modelado, no con la codificación, y esta compresión de texto usa solo la parte de codificación de WebP.
  • Quitar Google Fonts también mejoraría un poco el tiempo de carga de la página, porque se cargan desde un servidor remoto y requieren handshakes adicionales.

    • Pero si suficientes otros sitios también usan esa fuente, puede que ya esté en local.
  • Esta página se rompe al menos en el navegador de Sailfish OS. Hay un espacio vacío largo después del siguiente párrafo:
    “Alright, so we’re dealing with 92 KiB for gzip vs 37 + 71 KiB for Brotli. Umm…”
    Aun así, el overhead de comprimir HTML con gzip y Brotli no es nada comparado con la cantidad de JS, imágenes y video que usan los sitios web actuales.

    • Pasa lo mismo en Orion, Safari y LibreWolf. ¿Es una página solo para Chrome?
    • También pasa en Mull.
  • Personalmente no me gusta mucho este formato. Cuando guardo una imagen y se guarda como WebP, casi nada lo soporta fuera de los navegadores web, así que tengo que convertirla antes de editarla o usarla de forma significativa.
    Se siente como si simplemente me obligara a dar un paso extra.

    • Irónicamente, ni siquiera Slides, un producto de Google, soporta imágenes WebP.
      Aun así, si el soporte sigue aumentando, me parece bien. Puedo tolerar que aparezca un formato nuevo cada 20 años.
      .webm sí puede desaparecer.
    • Convertirlo toma 2 segundos. En macOS está literalmente en el menú de clic derecho y además el archivo pesa menos, así que no es realmente un problema.