OneImage
OneImage
·
technicalwebassemblyperformance

Construindo um Squoosh aprimorado: Compressão de imagens local de alto desempenho com libimagequant-wasm

Construindo um Squoosh aprimorado: Compressão de imagens local de alto desempenho com libimagequant-wasm

Um mergulho profundo em como construímos uma ferramenta de compressão de imagens de alto desempenho que respeita a privacidade, combinando a arquitetura do Google Squoosh com libimagequant-wasm, Web Workers e APIs web modernas.

Quando o Google anunciou o encerramento do Squoosh no início de 2023, a comunidade de desenvolvimento web perdeu uma ferramenta valiosa para compressão de imagens do lado do cliente. Na OneImage, vimos uma oportunidade não apenas de preservar essa funcionalidade, mas de aprimorá-la. Este artigo detalha a abordagem técnica que adotamos para construir uma versão aprimorada e pronta para produção do Squoosh que prioriza privacidade, desempenho e experiência do desenvolvedor.

Entendendo a arquitetura do Google Squoosh

O Google Squoosh foi pioneiro no conceito de executar codificadores de imagens inteiramente no navegador usando WebAssembly (WASM). A arquitetura original consistia em:

  • Processamento do lado do cliente: Toda compressão ocorre localmente, garantindo privacidade
  • Codecs WebAssembly: Codificadores de velocidade nativa compilados para WASM (MozJPEG, OxiPNG, WebP, AVIF)
  • Web Workers: Descarregamento de computação pesada para prevenir bloqueio de UI
  • Canvas API: Manipulação de imagens e geração de prévia

Embora revolucionário, o Squoosh tinha limitações:

  • A compressão PNG dependia apenas do OxiPNG, que priorizava taxa de compressão sobre velocidade
  • Sem capacidades de processamento em lote incorporadas
  • Configurações preset limitadas para casos de uso comuns
  • Lógica de UI e compressão fortemente acoplada

Nossa estratégia de aprimoramento

1. Integrando libimagequant-wasm para compressão PNG superior

A pedra angular do nosso aprimoramento é libimagequant-wasm, um port WebAssembly da biblioteca padrão da indústria pngquant. Esta biblioteca usa um sofisticado algoritmo de quantização de cores que produz resultados visualmente superiores comparado à redução simples de paleta.

Por que libimagequant?

  • Qualidade perceptual: Usa um algoritmo modificado de corte mediano otimizado para percepção humana
  • Paletas adaptativas: Gera paletas ótimas de 2-256 cores baseadas em conteúdo de imagem
  • Tratamento de transparência: Preserva canais alfa enquanto comprime
  • Desempenho: Executa em velocidade quase nativa graças ao WASM

Detalhes de implementação

Veja como integramos o libimagequant em nosso pipeline de compressão:

import LibImageQuant from '@fe-daily/libimagequant-wasm'; 
import * as wasmModule from '@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js';

async function compressPNG(imageData: ImageData, level: number): Promise<Uint8Array> {
  const quantizer = new LibImageQuant({ wasmModule });
  
  // Mapear nível de compressão (0-10) para contagem de cores (256-2)
  const maxColors = Math.max(2, 256 - (25.6 * level));
  
  const quantized = await quantizer.quantizeImageData(imageData, {
    maxColors: Math.floor(maxColors),
    speed: 1,          // Equilíbrio entre qualidade e velocidade
    quality: {
      min: 0,
      target: 100      // Buscar maior qualidade dentro do limite de cor
    }
  });
  
  return new Uint8Array(quantized.pngBytes);
}

Parâmetros principais explicados:

  • maxColors: Controla o tamanho da paleta. Menos cores = arquivo menor, mas potencialmente pior qualidade
  • speed: Faixa 1-10, onde 1 é mais lento mas maior qualidade
  • quality.target: Define o limiar de qualidade alvo (0-100)

Nossa ferramenta Squoosh usa esta implementação para entregar taxas de compressão de 60-80% com perda perceptual mínima.

2. Arquitetando um sistema Web Worker robusto

Para evitar que o navegador congele durante a compressão (especialmente para imagens grandes ou operações em lote), construímos uma arquitetura Web Worker dedicada:

// compression-worker.ts
import { EncoderOptions, CompressResult } from './squoosh-types';
import LibImageQuant from '@fe-daily/libimagequant-wasm'; 
import * as wasmModule from '@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js';

interface CompressMessage {
  type: 'compress';
  imageData: ImageData;
  options: EncoderOptions;
}

self.onmessage = async (e: MessageEvent<CompressMessage>) => {
  const { type, imageData, options } = e.data;

  if (type === 'compress') {
    try {
      const result = await compress(imageData, options);
      self.postMessage({ success: true, result });
    } catch (error) {
      self.postMessage({
        success: false,
        error: error instanceof Error ? error.message : 'Erro desconhecido',
      });
    }
  }
};

async function compress(
  imageData: ImageData,
  options: EncoderOptions
): Promise<CompressResult> {
  switch (options.type) {
    case 'png':
      return await compressPNGWithQuantization(imageData, options);
    case 'jpeg':
      return await compressJPEG(imageData, options);
    case 'webp':
      return await compressWebP(imageData, options);
    case 'avif':
      return await compressAVIF(imageData, options);
  }
}

Benefícios do Worker:

  • UI não bloqueante durante compressão
  • Capacidade de cancelar operações de longa duração
  • Processamento paralelo para operações em lote (múltiplos workers)
  • Isolamento de memória prevenindo vazamentos de memória da thread principal

3. Construindo presets de compressão inteligentes

Ao invés de expor parâmetros crus do codificador, criamos três presets otimizados para casos de uso comuns:

const PRESET_CONFIGS = {
  highQuality: {
    png: { level: 3 },     // ~200 cores
    jpeg: { quality: 90 },
    webp: { quality: 90 },
    avif: { quality: 85 }
  },
  balanced: {
    png: { level: 5 },     // ~128 cores
    jpeg: { quality: 80 },
    webp: { quality: 80 },
    avif: { quality: 70 }
  },
  minSize: {
    png: { level: 8 },     // ~50 cores
    jpeg: { quality: 60 },
    webp: { quality: 60 },
    avif: { quality: 50 }
  }
};

Estes presets foram calibrados através de testes extensivos em diversos tipos de imagens (fotos, ilustrações, capturas de tela, elementos de UI) para encontrar compensações ótimas de qualidade-tamanho.

4. Implementando processamento em lote eficiente

Para usuários comprimindo múltiplas imagens, construímos um sistema de fila com rastreamento de progresso:

class BatchCompressor {
  private queue: BatchItem[] = [];
  private activeWorkers: Set<Worker> = new Set();
  private maxConcurrency = navigator.hardwareConcurrency || 4;

  async processBatch(files: File[], options: ProcessorOptions) {
    const batchId = Date.now();
    
    for (const file of files) {
      this.queue.push({
        id: `${batchId}-${file.name}`,
        file,
        status: 'pending',
        options
      });
    }

    await this.processQueue();
  }

  private async processQueue() {
    while (this.queue.some(item => item.status === 'pending')) {
      if (this.activeWorkers.size < this.maxConcurrency) {
        const item = this.queue.find(i => i.status === 'pending');
        if (item) {
          item.status = 'processing';
          await this.processItem(item);
        }
      } else {
        await new Promise(resolve => setTimeout(resolve, 100));
      }
    }
  }
}

Esta abordagem:

  • Maximiza utilização de CPU executando workers paralelos
  • Previne travamentos do navegador limitando concorrência
  • Fornece atualizações de progresso em tempo real aos usuários

Experimente nosso processamento em lote em ação no OneImage Squoosh.

Otimizações de desempenho

Gerenciamento de memória

Imagens grandes podem rapidamente esgotar a memória do navegador. Implementamos várias estratégias de mitigação:

async function processLargeImage(file: File): Promise<CompressResult> {
  const MAX_DIMENSION = 4096;
  const img = await loadImage(file);
  
  // Reduzir escala se necessário
  let { width, height } = img;
  if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
    const scale = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height);
    width = Math.floor(width * scale);
    height = Math.floor(height * scale);
  }
  
  // Usar OffscreenCanvas quando disponível para melhor tratamento de memória
  const canvas = typeof OffscreenCanvas !== 'undefined'
    ? new OffscreenCanvas(width, height)
    : document.createElement('canvas');
  
  canvas.width = width;
  canvas.height = height;
  
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0, width, height);
  
  const imageData = ctx.getImageData(0, 0, width, height);
  
  // Liberar recursos de canvas e imagem imediatamente
  URL.revokeObjectURL(img.src);
  
  return await compress(imageData, options);
}

Cache de módulos WASM

Módulos WebAssembly se beneficiam de cache agressivo:

let cachedWasmModule: typeof wasmModule | null = null;

async function getWasmModule() {
  if (!cachedWasmModule) {
    cachedWasmModule = await import(
      '@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js'
    );
  }
  return cachedWasmModule;
}

Isso reduz o tempo de inicialização de ~500ms para quase instantâneo em compressões subsequentes.

Expandindo o conjunto de ferramentas

Enquanto o Squoosh foca em compressão, construímos uma suíte completa de ferramentas complementares:

Todas as ferramentas compartilham os mesmos princípios arquiteturais: privacidade em primeiro lugar, impulsionadas por WASM e completamente do lado do cliente.

Integração de extensão de navegador

Estendemos a arquitetura do aplicativo web para uma extensão de navegador, habilitando:

  • Acesso instantâneo via popup da barra de ferramentas
  • Integração de menu de contexto para compressão com clique direito
  • Gerenciamento de estado baseado em abas
  • Armazenamento local para preferências de presets

A extensão reutiliza a mesma infraestrutura Web Worker e WASM, garantindo comportamento consistente em todas as plataformas.

Implantação e infraestrutura

Edge Computing com Cloudflare

Implantamos o OneImage Squoosh no Cloudflare Pages, aproveitando:

  • CDN global para tempos de carregamento inicial <50ms em todo o mundo
  • Compressão HTTP/3 e Brotli para recursos
  • Cabeçalhos de cache inteligentes para módulos WASM
  • Zero cold starts (apenas recursos estáticos)

Otimização de build

Nossa configuração Next.js inclui:

// next.config.ts
module.exports = {
  webpack: (config, { isServer }) => {
    // Suporte para arquivos .wasm
    config.experiments = {
      asyncWebAssembly: true,
      layers: true,
    };
    
    // Otimizar imports de worker
    config.module.rules.push({
      test: /\.worker\.(ts|js)$/,
      use: { loader: 'worker-loader' }
    });
    
    return config;
  },
  
  // Divisão de código agressiva
  experimental: {
    optimizePackageImports: [
      '@jsquash/jpeg',
      '@jsquash/png',
      '@jsquash/webp',
      '@jsquash/avif'
    ]
  }
};

Isso garante que codificadores sejam carregados sob demanda, mantendo o bundle inicial abaixo de 100KB (gzipado).

Testes e garantia de qualidade

Mantemos cobertura de testes abrangente para lógica de compressão:

// __tests__/advanced-compressor.test.ts
describe('AdvancedImageCompressor', () => {
  it('deve comprimir PNG com libimagequant', async () => {
    const compressor = new AdvancedImageCompressor();
    const mockFile = createMockImageFile('test.png', 1000, 1000);
    
    const result = await compressor.compress(mockFile, {
      encode: { type: 'png', options: { level: 5 } }
    });
    
    expect(result.format).toBe('png');
    expect(result.size).toBeLessThan(mockFile.size);
    expect(result.data).toBeInstanceOf(Uint8Array);
  });
  
  it('deve lidar com processamento em lote com concorrência', async () => {
    const files = Array(10).fill(null).map((_, i) => 
      createMockImageFile(`test-${i}.png`, 500, 500)
    );
    
    const startTime = Date.now();
    await batchCompress(files, { preset: 'balanced' });
    const duration = Date.now() - startTime;
    
    // Deve ser mais rápido que processamento sequencial
    expect(duration).toBeLessThan(10 * 1000); // <1s por imagem
  });
});

Lições aprendidas

  1. WASM está pronto para produção: Com carregamento e cache de módulo adequados, o desempenho do WASM rivaliza com aplicações nativas
  2. Web Workers são essenciais: Para qualquer tarefa intensiva em CPU, descarregar para workers não é negociável
  3. Presets de usuário > controles brutos: A maioria dos usuários prefere "bons padrões" ao ajuste granular
  4. Memória importa: Sempre faça perfil do uso de memória em imagens grandes e implemente salvaguardas
  5. Privacidade vende: Enfatizar "sem uploads ao servidor" ressoa fortemente com os usuários

Código aberto e comunidade

Embora o OneImage Squoosh seja um produto comercial, contribuímos para o ecossistema:

  • Relatórios de bugs e PRs para mantenedores do @jsquash
  • Melhorias de documentação para libimagequant-wasm
  • Compartilhamento de benchmarks de desempenho e melhores práticas

Conclusão

Construir um Squoosh aprimorado exigiu mais do que simplesmente integrar libimagequant-wasm. Demandou decisões arquiteturais cuidadosas em torno de Web Workers, gerenciamento de memória, experiência do usuário e infraestrutura de implantação. O resultado é uma ferramenta que respeita a privacidade do usuário enquanto entrega desempenho de compressão de nível profissional.

Experimente OneImage Squoosh hoje, ou explore nossa extensão de navegador para acesso ainda mais rápido. Para desenvolvedores construindo ferramentas similares, esperamos que este mergulho técnico forneça um blueprint útil.

---

Referências e leitura adicional