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 qualidadespeed: Faixa 1-10, onde 1 é mais lento mas maior qualidadequality.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:
- Image Resize: Escalonamento inteligente com múltiplos algoritmos
- EXIF Remover: Remover metadados para privacidade
- Image Overlay: Marca d'água e branding
- Image Blur: Ocultação para conteúdo sensível
- Crop Tool: Recorte preciso de proporção de aspecto
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
- WASM está pronto para produção: Com carregamento e cache de módulo adequados, o desempenho do WASM rivaliza com aplicações nativas
- Web Workers são essenciais: Para qualquer tarefa intensiva em CPU, descarregar para workers não é negociável
- Presets de usuário > controles brutos: A maioria dos usuários prefere "bons padrões" ao ajuste granular
- Memória importa: Sempre faça perfil do uso de memória em imagens grandes e implemente salvaguardas
- 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
- Google Squoosh (arquivado) - Repositório do projeto original
- libimagequant-wasm - Port WebAssembly do pngquant
- pngquant - Biblioteca libimagequant original
- @jsquash - Coleção de codecs de imagem WebAssembly
- Documentação WebAssembly - Mozilla Developer Network
- Web Workers API - Guia MDN
- Canvas API - Referência de manipulação de imagens
- Melhores práticas de compressão de imagens - Guia web.dev
- OneImage Squoosh - Experimente a ferramenta
- Extensão de navegador OneImage - Versão de extensão de navegador
- Guia de formatos de imagem - Comparação abrangente de formatos
