1 pontos por GN⁺ 2024-09-08 | 1 comentários | Compartilhar no WhatsApp
  • Como o GitHub Pages não oferece suporte a Brotli, este é um experimento que codifica HTML como uma imagem WebP sem perdas e o restaura com o decodificador de imagens do navegador e JavaScript para reduzir o volume de dados transferidos
  • Um decodificador Brotli em WASM adiciona um custo extra de 71 KiB~200 KB, e a Compression Streams API só suporta gzip, deflate e deflate-raw, o que dificulta usá-la como atalho para decodificação de Brotli
  • O WebP sem perdas VP8L pode gerar resultados menores que gzip até para dados textuais graças à transformação preditiva, ao reuso de árvores de Huffman por blocos de 16x16 e ao color cache
  • Um HTML de teste com 439,478 bytes foi reduzido para 94,683 bytes com gzip e até 43,182 bytes com WebP; ainda maior que os 37 KiB do Brotli, mas cerca de 2,2x menor que gzip
  • Por causa do ruído anti-fingerprinting do Canvas 2D, da mudança no readPixels do WebGL, da tela em branco inicial e de problemas na restauração de rolagem, isso ficou mais próximo de um hack que revela limitações do navegador do que de algo aplicável na prática

O problema de não poder usar Brotli no GitHub Pages

  • Ao reduzir o tempo de carregamento de uma página, a compressão HTTP tem efeito maior do que minificar o HTML
  • O HTTP oferece suporte a gzip e Brotli por meio do cabeçalho Content-Encoding
    • gzip é barato, então costuma vir ativado por padrão
    • Brotli normalmente comprime melhor que gzip, mas é muito mais lento
  • Como o GitHub Pages não oferece suporte a Brotli, o texto mais longo do site, Recovering garbled Bitcoin addresses, fica com 92 KiB em gzip em vez de poder ter 37 KiB com Brotli
  • Essa diferença aumenta desnecessariamente o tempo de carregamento em 2,5x

As primeiras alternativas imaginadas e onde elas emperraram

  • Se o GitHub permitisse enviar e servir arquivos Brotli pré-comprimidos, o problema poderia ser evitado, mas esse recurso não existe
  • Descompactar no cliente diretamente com JavaScript perde vantagem por causa do tamanho do decodificador WASM
  • A pilha HTTP do navegador já tem um decodificador Brotli, mas o DecompressionStream da Compression Streams API só aceita gzip, deflate e deflate-raw
  • Mesmo pré-comprimindo gzip com Zopfli, o resultado foi 86 KiB, ainda maior que Brotli

Usando um formato de imagem como contêiner de compressão

  • Como o navegador já consegue decodificar imagens, é possível colocar os dados nos pixels da imagem e lê-los de volta com a Canvas API sem adicionar uma lógica nova de descompressão
  • GIF organiza os dados em ordem row-major e depois aplica LZW, mas o DEFLATE do gzip foi projetado justamente para substituir o LZW, então é difícil esperar vantagem
  • PNG usa DEFLATE, mas antes aplica uma transformação preditiva que comprime não os pixels originais, e sim a diferença em relação aos pixels vizinhos
    • Ex.: compactar [a, b, c, d] em vez de [a, b-a, c-b, d-c]
    • Quanto menor a diferença entre o valor previsto e o real, mais favorável fica a compressão com Huffman
  • O núcleo do experimento é usar o VP8L, a variante sem perdas do WebP, para comprimir dados genéricos de bytes

Em que o VP8L difere do gzip

  • O WebP tem variantes com e sem perdas, e aqui só entra o formato sem perdas VP8L
  • Assim como PNG, o VP8L usa transformação preditiva, mas no lugar do DEFLATE usa um método parecido, criado pelo Google
  • O DEFLATE divide o arquivo em vários trechos e pode usar árvores de Huffman adequadas a cada um deles
    • Se um HTML mistura JavaScript, SVG e markup, árvores diferentes podem ser vantajosas
  • O VP8L define uma tabela potencialmente grande de árvores de Huffman e pode usar árvores diferentes para cada bloco de 16x16 pixels
    • Quando há JavaScript, depois CSS, depois JavaScript de novo, o DEFLATE pode acabar codificando árvores parecidas várias vezes
    • O VP8L consegue reutilizar árvores, então pode trocá-las com mais frequência e a custo menor
  • O color cache do VP8L permite representar valores de forma curta, como “copie um pixel recente com certa propriedade”

Primeiro experimento de compressão com WebP

  • O arquivo de teste era o HTML de Recovering garbled Bitcoin addresses
    • Tamanho original: 439,478 bytes
    • gzip --best: 94,683 bytes
  • Com a crate webp do Rust, os bytes foram convertidos em uma imagem RGB em tons de cinza e comprimidos em WebP sem perdas
  • O motivo para usar grayscale foi a transformação subtract green do WebP
    • Em grayscale, ao subtrair o canal G de R/B, R/B viram praticamente 0
    • Como o WebP codifica os três canais com árvores de Huffman separadas, canais com valor fixo ocupam espaço próximo de O(1)
  • A ideia inicial era criar uma imagem 1xN, mas o WebP só suporta até 16383x16383, então ocorreu o erro VP8_ENC_ERROR_BAD_DIMENSION
  • Ajustando para o formato 16383xN, o resultado ficou em 45,604 bytes, 2x menor que gzip e até menor que 49,764 bytes do bzip2

Ajustes específicos para WebP

  • Em uma imagem larga, usar ordem row-major mistura dentro de um bloco 16x16 bytes que estavam distantes entre si na entrada
  • Ao mudar o formato da imagem para um 27x16383 alto e estreito, a compressão caiu para 43,232 bytes
  • Foram comparadas as configurações method do cwebp, de 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
  • O method 5 foi escolhido por produzir o mesmo tamanho do method 6, mas ser mais rápido
  • Nesse ponto, o WebP ficou 2,2x menor que gzip e 1,2x maior que Brotli

Benchmark com vários arquivos

  • Os alvos de comparação foram o snappy testdata, o Canterbury Corpus e Large Corpus e 2 arquivos SVG
  • Os formatos comparados foram gzip --best, brotli --best, bzip2 --best e o script de compressão com WebP
  • Tirando arquivos muito pequenos como grammar.lsp, xargs.1 e algumas exceções, o WebP quase sempre foi melhor que gzip
  • As exceções foram kennedy.xls e paper-100k.pdf
    • Em paper-100k.pdf, há 19 KB de XML seguidos por dados já comprimidos, então na prática o teste mede um volume pequeno de dados
    • kennedy.xls também tem desempenho estranho em relação a Brotli e bzip2 e pode ser um arquivo difícil para compressores, com muitos dados heterogêneos em posições próximas
  • O WebP tende a ser um pouco pior que bzip2, embora em alguns casos consiga superar
  • O WebP sempre foi pior que Brotli, exceto em casos como fireworks.jpeg, que é quase um blob aleatório uniforme
  • Em grandes volumes de texto puro, ele mostrou melhora mensurável em relação ao gzip
    • Houve melhora também nos arquivos SVG
    • Em html_x_4, o WebP atingiu taxa de compressão de 3.3%, pior que os 2.8% do Brotli, mas muito melhor que os 13% do gzip

Restaurando com JavaScript

  • A decodificação do WebP em si pode ser implementada com fetch, createImageBitmap, OffscreenCanvas, getImageData e TextDecoder
  • A estrutura usa o canal R dos pixels como bytes do HTML original, decodifica isso em UTF-8 e depois injeta o resultado em document.documentElement.innerHTML
  • A Canvas API é muito usada para fingerprinting, então alguns navegadores adicionam ruído ao resultado de getImageData
    • No modo strict tracking protection do Firefox, menos de 1% dos pixels podem ser afetados
    • No HTML, esse ruído aparece como se fossem erros de digitação
  • Usando readPixels do WebGL, na época isso funcionava sem ruído
    • O WebGL só oferece suporte confiável a texturas de até 2048x2048, então foi preciso ajustar novamente os limites de tamanho
    • Esse código de restauração, após minificação, tinha cerca de 550 bytes
  • Somando WebP e código, o total ficou em 44 KiB, comparado com 92 KiB do gzip e 37 KiB do Brotli
  • Depois de 15 de abril de 2026, o Firefox passou a aplicar anti-fingerprinting também ao readPixels, então esse método deixou de funcionar como antes

Cintilação de tela e problema de rolagem

  • Como await funciona com promises, o navegador entende que a execução do script terminou antes do fim do download do WebP
  • Como o DOM ainda está vazio, o usuário vê por um instante uma tela branca vazia
  • Como mitigação, o estilo e cerca de 8 KiB do topo da página podem permanecer no HTML em gzip, deixando só o conteúdo abaixo da viewport comprimido em WebP
  • Restaurar a posição da rolagem ao recarregar também vira um problema
    • Por exemplo, se a página for recarregada em Y = 5000px, mas a altura da página ainda for 0px, a posição é resetada
    • Adicionar uma div temporária muito alta ajuda
  • É preciso atribuir o HTML a document.documentElement.innerHTML em vez de usar document.write, para atualizar o documento atual sem trocá-lo por um novo

Colocando o WebP diretamente no JavaScript

  • Para reduzir ainda mais a latência, é possível embutir o WebP diretamente no JavaScript
  • A forma mais simples é uma data URL em base64
  • O base64 aumenta o tamanho original em 1,33x, mas o gzip praticamente compensa esse aumento
    • Ao converter compressed.webp para base64, ele passa a ter 57,576 bytes
    • Com gzip --best, isso cai para 43,519 bytes
  • Um blob comprimido como WebP é quase um conjunto uniforme de dados aleatórios, e a conversão de 8 bits para 6 bits do base64 faz com que a árvore de Huffman do gzip atue quase como uma transformação inversa
  • Também daria para usar Unicode e UTF-16, mas o base64 já serve como primeira solução suficiente

Aplicação real e situação atual

  • No momento em que o texto foi escrito, a própria página estava comprimida em WebP a partir da seção “Fool me twice”, a menos que o navegador fosse antigo ou o JavaScript estivesse desativado
  • A imagem WebP da página real, no código, era alta e estreita, mas também foi fornecido um exemplo quadrado de WebP para facilitar a visualização
  • Na imagem, as áreas claras no topo e na base eram texto e código, a região hachurada perto de 1/5 da altura era um diagrama, e a maior parte das áreas escuras era texto dentro do diagrama
  • O ganho real acabou sendo limitado
    • Página original com gzip: 88 KiB
    • Página com WebP aplicada, ainda em gzip: 83 KiB
    • Estimativa com Brotli: 69 KiB
  • Depois de 15 de abril de 2026, para evitar que visitantes do Firefox vissem conteúdo corrompido, a página voltou a ficar sem essa compressão
  • O código em Rust, os corpora e outros arquivos estão disponíveis no GitHub

1 comentários

 
GN⁺ 2024-09-08
Opiniões no Hacker News
  • Se você ignorar a latência, até faria sentido, mas na prática parece algo como um aumento de 0,001% no tempo de carregamento
    O aumento de tamanho é insignificante em comparação com a latência de ida e volta, e o tempo gasto na descompressão pode até ser maior do que o tempo economizado ao transmitir 55KiB a menos
    É um experimento interessante, mas neste caso a experiência do usuário provavelmente ficaria pior; a velocidade seria quase a mesma e só a compatibilidade cairia

    • Isso está certo se você só otimizar o tempo de carregamento e presumir a velocidade de dados de todos, mas muitas vezes eu preferiria que autores de sites ou apps não decidissem tão facilmente por mim esse compromisso entre velocidade e dados
      O problema é que há gente, como o autor do TFA, se esforçando para reduzir 100KB para 50KB, mas também há lugares que me enviam dezenas de MB em imagens sem pensar duas vezes quando eu só quero ver o horário de funcionamento de um restaurante usando dados em roaming
      A consciência de recursos existe, mas infelizmente está distribuída de forma muito desigual
    • Não é só uma questão de tempo de descompressão. Você só pode descomprimir depois de baixar tudo, mas o navegador consegue descomprimir e renderizar imediatamente o HTML conforme ele é transmitido pelo servidor
      Se a conexão cair, você perde tudo, e nem consegue ler apenas a parte que foi baixada
      Em conexões comuns, a diferença não é significativa; em conexões muito lentas ou instáveis, nas quais 50KB importam, essa abordagem é claramente pior. É um experimento interessante, mas eu preferiria que não fosse aplicado a sites
    • Se não estiver no cache, há um arquivo Symbols-2048-em%20Nerd%20Font%20Complete.woff2 de 850K, o que praticamente anula essa diferença
    • Uma diferença de tamanho desse nível é grande o bastante para afetar o número de idas e voltas necessárias. Com um valor moderno e razoável de janela inicial de congestionamento, deveria reduzir aproximadamente uma ida e volta
      Não será uma diferença de 2,5x, mas também não é 0,001%
    • Se a economia for menor do que uma janela de recepção TCP, não haverá diferença na latência
      Em redes com perdas talvez faça diferença, mas não tenho certeza
  • Não entendo por que readPixels não é alvo de proteção contra fingerprinting. Para mim tudo bem, já que não costumo espalhar erros de digitação quase invisíveis pela página inteira
    A parte que diz que no HTML gzipado ficam apenas os estilos e cerca de 8KiB do topo da página, enquanto só o conteúdo abaixo da viewport é comprimido em WebP, explica por que o texto foi cortado de repente depois de uma frase aleatória e uma página em branco continuou em seguida
    Estou usando LibreWolf, então o WebGL está desativado, e uso Chromium para jogos aleatórios da web que precisam de WebGL. Ao ativar o WebGL, o texto funcionou corretamente e, sinceramente, é uma técnica bem elegante

    • Se não funciona em todos os navegadores web modernos, nem mesmo com proteção contra fingerprinting ativada, e não há fallback para navegadores antigos, é difícil chamar isso de elegante
      A WWW deve ser universalmente acessível, começando com HTML simples e sendo aprimorada progressivamente
  • Também é possível usar Brotli diretamente em navegadores web, mas obviamente há limitações
    Acredito que a submissão ao JS1024 de 2022 [1] tenha sido a primeira demonstração desse conceito, e também há um código de prova de conceito para compressão arbitrária. Infelizmente, ele não serviu para o objetivo original, que era code golfing de tamanho
    A limitação central é que, na prática, fica restrito a caracteres ASCII e, por motivos óbvios, é muito sensível à pilha de renderização. No momento parece não funcionar no Firefox

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

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

    • O ponto-chave para entender essa abordagem, sem precisar mergulhar em como ela foi enfiada ali na prática, é o seguinte
      Só é possível usar o formato de arquivo de fonte WOFF2, para o qual o Brotli foi originalmente projetado, mas para aproveitá-lo é preciso criar um arquivo de fonte inteiro
      Navegadores recentes geralmente higienizam fontes com o OpenType Sanitizer (OTS), porque colocar diretamente no sistema arquivos de fonte não confiáveis é muito perigoso; por isso, é necessário criar um arquivo WOFF2 que seja normal o bastante para o OTS aceitar, mas que ainda contenha internamente a sequência de bytes desejada e permita extraí-la
      Depois de muitos fracassos, a ideia de se fixar nos advance widths, isto é, nas larguras dos glifos, que são codificadas como uma sequência de inteiros com sinal de 2 bytes quase sem restrições, é fantástica
    • Corrigindo: ainda funciona no Firefox. Eu apenas tinha esquecido que, no Firefox, a taxa de zoom precisa ser exatamente 100%
    • Essa técnica é realmente surpreendente e muito mais legal do que meu texto. Meus elogios
  • O lado do Chromium bloqueou isso por muito tempo, mas o zstd também está chegando à web. Agora que finalmente entrou no Chrome, só falta o Safari acompanhar

    • Eu gostaria que tudo migrasse para Zstandard, mas neste caso específico, pelo que sei, quando o uso de memória do descompressor é o mesmo, Brotli e Zstandard ficam quase empatados
    • Pelo menos parece estar na lista de tarefas: https://webkit.org/standards-positions/#position-168
  • Estou criando o Batch Compress (https://batchcompress.com/en) e, pouco depois de adicionar suporte a WebP, recentemente mudei para que ele fosse o padrão
    Pelo que eu sei, eu já estava gerando os JPEGs menores entre as ferramentas de compressão para a web, mas o WebP saía com apenas cerca de 50% do tamanho do JPEG. Depois de adicionar o suporte, foi uma decisão fácil torná-lo o padrão
    Como o site tem bastante usuários, achei que haveria algumas reclamações depois de mudar o padrão para WebP, mas agora, cerca de um mês depois, só houve uma pergunta ou reclamação relacionada a WebP
    Parece que hoje quase todas as ferramentas e navegadores já têm suporte a WebP. Só vi recentemente um site que não conseguia lidar direito com uploads de imagens WebP, bloqueando a próxima etapa; fora isso, hoje em dia quase tudo dá suporte bem

    • Se o WebP reduz o tamanho do arquivo em mais de 15 a 20% em relação ao JPEG, essa economia vem de perda de qualidade, não de melhoria na compressão
      Se você comprimir e otimizar bem o JPEG, ele não deveria ficar muito atrás do WebP
      Você sempre pode reduzir o tamanho do arquivo criando um WebP que pareça quase igual ao JPEG, mas o mesmo vale se recomprimir como um JPEG que pareça quase igual
      Isso é uma característica de todos os codecs de compressão com perdas e, como o tamanho do arquivo cresce exponencialmente conforme a qualidade aumenta, as pessoas sempre se surpreendem com o quanto uma perda de qualidade mínima, quase imperceptível, pode mudar bastante o tamanho do arquivo
    • Fico curioso para saber com base em qual métrica de comparação de qualidade se diz que o WebP tem cerca de 50% do tamanho do JPEG
      O WebP tinha fama de ter padrões horríveis que destruíam detalhes em áreas escuras
  • Olhando o código-fonte, vi que está faltando um espaço na declaração do doctype. A forma atual está errada; deveria haver um espaço

  • Já usei esse truque antes. Curiosamente não me lembro em quê, mas acho que era só para ver se era possível, e também deixei um comentário aqui: https://gist.github.com/gasman/2560551?permalink_comment_id=...
    Também encontrei um protótipo antigo, que acho que era só um teste: https://retr0.id/stuff/bee_movie.webp.html

    • Essa página quebrou minha extensão de gestos do mouse
      Hoje esse tipo de injeção de script é chamado de extensão, não de add-on, mas, de todo modo, achei interessante a abordagem de entregar “lixo” no começo e anexar JS depois para devolver à página
      O nerd de segurança dentro de mim fica se perguntando se isso poderia viabilizar um ataque quando houver dados fornecidos pelo usuário, como um formulário de comentários
      Talvez alguém conseguisse descobrir a sequência de bytes a colocar em um comentário para que, depois da compressão, ela virasse uma tag script posicionada antes do meu script e fosse executada
    • Pela minha experiência, WebP não se encaixava bem no caso geral em que essa técnica seria realmente útil, ou seja, dados abaixo de 10 KB
      A maior parte do que o WebP lossless acrescenta ao PNG diz respeito à modelagem, não à codificação, e esse tipo de compressão de texto acaba usando apenas a parte de codificação do WebP
  • Remover o Google Fonts também melhoraria um pouco o tempo de carregamento da página, pois ele é carregado de um servidor remoto e exige um handshake adicional

    • Mas, se um número suficiente de outros sites também usar essa fonte, ela talvez já esteja localmente
  • Esta página quebra pelo menos no navegador do Sailfish OS. Há um grande espaço em branco depois do seguinte parágrafo
    “Alright, so we’re dealing with 92 KiB for gzip vs 37 + 71 KiB for Brotli. Umm…”
    Ainda assim, o overhead da compressão HTML com gzip e Brotli não é nada comparado à quantidade de JS, imagens e vídeo que os sites usam hoje

    • Acontece o mesmo no Orion, Safari e LibreWolf. Esta página é exclusiva para Chrome?
    • Acontece o mesmo no Mull
  • Pessoalmente, não gosto muito desse formato. Quando salvo uma imagem e ela é salva como WebP, quase nada além do navegador oferece suporte, então preciso convertê-la antes de editar ou usar de forma significativa
    Parece apenas uma etapa extra sendo imposta

    • Ironicamente, nem o Slides, produto do Google, oferece suporte a imagens WebP
      Ainda assim, se o suporte aumentar, acho aceitável. Dá para tolerar um novo formato surgindo uma vez a cada 20 anos
      .webm pode desaparecer
    • Converter leva 2 segundos. No macOS, literalmente está no menu de clique direito e, como o tamanho também é menor, não chega a ser um problema