Cuando Google anunció el cierre de Squoosh a principios de 2023, la comunidad de desarrollo web perdió una herramienta valiosa para compresión de imágenes del lado del cliente. En OneImage, vimos una oportunidad no solo de preservar esta funcionalidad, sino de mejorarla. Este artículo detalla el enfoque técnico que adoptamos para construir una versión mejorada y lista para producción de Squoosh que prioriza privacidad, rendimiento y experiencia del desarrollador.
Entendiendo la arquitectura de Google Squoosh
Google Squoosh fue pionero en el concepto de ejecutar codificadores de imágenes completamente en el navegador usando WebAssembly (WASM). La arquitectura original consistía en:
- Procesamiento del lado del cliente: Toda la compresión ocurre localmente, asegurando privacidad
- Codecs WebAssembly: Codificadores de velocidad nativa compilados a WASM (MozJPEG, OxiPNG, WebP, AVIF)
- Web Workers: Descarga de computación pesada para prevenir bloqueo de UI
- Canvas API: Manipulación de imágenes y generación de vista previa
Aunque revolucionario, Squoosh tenía limitaciones:
- La compresión PNG dependía solo de OxiPNG, que priorizaba ratio de compresión sobre velocidad
- Sin capacidades de procesamiento por lotes incorporadas
- Configuraciones preestablecidas limitadas para casos de uso comunes
- Lógica de UI y compresión estrechamente acoplada
Nuestra estrategia de mejora
1. Integrando libimagequant-wasm para compresión PNG superior
La piedra angular de nuestra mejora es libimagequant-wasm, un port de WebAssembly de la biblioteca estándar de la industria pngquant. Esta biblioteca usa un sofisticado algoritmo de cuantización de color que produce resultados visualmente superiores comparado con la reducción simple de paleta.
¿Por qué libimagequant?
- Calidad perceptual: Usa un algoritmo modificado de corte mediano optimizado para percepción humana
- Paletas adaptativas: Genera paletas óptimas de 2-256 colores basadas en contenido de imagen
- Manejo de transparencia: Preserva canales alfa mientras comprime
- Rendimiento: Se ejecuta a velocidad casi nativa gracias a WASM
Detalles de implementación
Así es como integramos libimagequant en nuestro pipeline de compresión:
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 nivel de compresión (0-10) a conteo de colores (256-2)
const maxColors = Math.max(2, 256 - (25.6 * level));
const quantized = await quantizer.quantizeImageData(imageData, {
maxColors: Math.floor(maxColors),
speed: 1, // Balance entre calidad y velocidad
quality: {
min: 0,
target: 100 // Apuntar a la mayor calidad dentro del límite de color
}
});
return new Uint8Array(quantized.pngBytes);
}
Parámetros clave explicados:
maxColors: Controla el tamaño de la paleta. Menos colores = archivo más pequeño, pero potencialmente peor calidadspeed: Rango 1-10, donde 1 es más lento pero mayor calidadquality.target: Establece el umbral de calidad objetivo (0-100)
Nuestra herramienta Squoosh usa esta implementación para ofrecer ratios de compresión de 60-80% con mínima pérdida perceptual.
2. Arquitectura de un sistema Web Worker robusto
Para evitar que el navegador se congele durante la compresión (especialmente para imágenes grandes u operaciones por lotes), construimos una arquitectura 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 : 'Error desconocido',
});
}
}
};
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);
}
}
Beneficios del Worker:
- UI no bloqueante durante compresión
- Capacidad de cancelar operaciones de larga duración
- Procesamiento paralelo para operaciones por lotes (múltiples workers)
- Aislamiento de memoria que previene fugas de memoria del hilo principal
3. Construyendo presets de compresión inteligentes
En lugar de exponer parámetros de codificador crudos, creamos tres presets optimizados para casos de uso comunes:
const PRESET_CONFIGS = {
highQuality: {
png: { level: 3 }, // ~200 colores
jpeg: { quality: 90 },
webp: { quality: 90 },
avif: { quality: 85 }
},
balanced: {
png: { level: 5 }, // ~128 colores
jpeg: { quality: 80 },
webp: { quality: 80 },
avif: { quality: 70 }
},
minSize: {
png: { level: 8 }, // ~50 colores
jpeg: { quality: 60 },
webp: { quality: 60 },
avif: { quality: 50 }
}
};
Estos presets fueron calibrados a través de pruebas extensivas en diversos tipos de imágenes (fotos, ilustraciones, capturas de pantalla, elementos UI) para encontrar compensaciones óptimas de calidad-tamaño.
4. Implementando procesamiento por lotes eficiente
Para usuarios comprimiendo múltiples imágenes, construimos un sistema de cola con seguimiento de progreso:
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));
}
}
}
}
Este enfoque:
- Maximiza utilización de CPU ejecutando workers paralelos
- Previene caídas del navegador limitando concurrencia
- Proporciona actualizaciones de progreso en tiempo real a usuarios
Prueba nuestro procesamiento por lotes en acción en OneImage Squoosh.
Optimizaciones de rendimiento
Gestión de memoria
Las imágenes grandes pueden agotar rápidamente la memoria del navegador. Implementamos varias estrategias de mitigación:
async function processLargeImage(file: File): Promise<CompressResult> {
const MAX_DIMENSION = 4096;
const img = await loadImage(file);
// Reducir escala si es necesario
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 cuando esté disponible para mejor manejo de memoria
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 imagen inmediatamente
URL.revokeObjectURL(img.src);
return await compress(imageData, options);
}
Caché de módulos WASM
Los módulos WebAssembly se benefician de caché agresivo:
let cachedWasmModule: typeof wasmModule | null = null;
async function getWasmModule() {
if (!cachedWasmModule) {
cachedWasmModule = await import(
'@fe-daily/libimagequant-wasm/wasm/libimagequant_wasm.js'
);
}
return cachedWasmModule;
}
Esto reduce el tiempo de inicialización de ~500ms a casi instantáneo en compresiones subsecuentes.
Expandiendo el conjunto de herramientas
Mientras Squoosh se enfoca en compresión, construimos una suite completa de herramientas complementarias:
- Image Resize: Escalado inteligente con múltiples algoritmos
- EXIF Remover: Eliminar metadatos para privacidad
- Image Overlay: Marca de agua y branding
- Image Blur: Ocultación para contenido sensible
- Crop Tool: Recorte preciso de relación de aspecto
Todas las herramientas comparten los mismos principios arquitectónicos: privacidad primero, impulsadas por WASM y completamente del lado del cliente.
Integración de extensión de navegador
Extendimos la arquitectura de la aplicación web a una extensión de navegador, habilitando:
- Acceso instantáneo a través de popup de barra de herramientas
- Integración de menú contextual para compresión con clic derecho
- Gestión de estado basada en pestañas
- Almacenamiento local para preferencias de presets
La extensión reutiliza la misma infraestructura Web Worker y WASM, asegurando comportamiento consistente en todas las plataformas.
Despliegue e infraestructura
Edge Computing con Cloudflare
Desplegamos OneImage Squoosh en Cloudflare Pages, aprovechando:
- CDN global para tiempos de carga inicial <50ms en todo el mundo
- Compresión HTTP/3 y Brotli para recursos
- Encabezados de caché inteligentes para módulos WASM
- Cero arranques en frío (solo recursos estáticos)
Optimización de construcción
Nuestra configuración Next.js incluye:
// next.config.ts
module.exports = {
webpack: (config, { isServer }) => {
// Soporte para archivos .wasm
config.experiments = {
asyncWebAssembly: true,
layers: true,
};
// Optimizar imports de worker
config.module.rules.push({
test: /\.worker\.(ts|js)$/,
use: { loader: 'worker-loader' }
});
return config;
},
// División de código agresiva
experimental: {
optimizePackageImports: [
'@jsquash/jpeg',
'@jsquash/png',
'@jsquash/webp',
'@jsquash/avif'
]
}
};
Esto asegura que los codificadores se carguen bajo demanda, manteniendo el bundle inicial bajo 100KB (gzippeado).
Testing y aseguramiento de calidad
Mantenemos cobertura de pruebas integral para lógica de compresión:
// __tests__/advanced-compressor.test.ts
describe('AdvancedImageCompressor', () => {
it('debería comprimir PNG con 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('debería manejar procesamiento por lotes con concurrencia', 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;
// Debería ser más rápido que procesamiento secuencial
expect(duration).toBeLessThan(10 * 1000); // <1s por imagen
});
});
Lecciones aprendidas
- WASM está listo para producción: Con carga y caché de módulos adecuados, el rendimiento de WASM rivaliza con aplicaciones nativas
- Los Web Workers son esenciales: Para cualquier tarea intensiva en CPU, descargar a workers es no negociable
- Presets de usuario > controles crudos: La mayoría de los usuarios prefieren "buenos valores predeterminados" sobre ajuste granular
- La memoria importa: Siempre perfila el uso de memoria en imágenes grandes e implementa salvaguardas
- La privacidad vende: Enfatizar "sin cargas al servidor" resuena fuertemente con los usuarios
Código abierto y comunidad
Aunque OneImage Squoosh es un producto comercial, contribuimos al ecosistema:
- Informes de errores y PRs a mantenedores de @jsquash
- Mejoras de documentación para libimagequant-wasm
- Compartir benchmarks de rendimiento y mejores prácticas
Conclusión
Construir un Squoosh mejorado requirió más que simplemente integrar libimagequant-wasm. Demandó decisiones arquitectónicas cuidadosas alrededor de Web Workers, gestión de memoria, experiencia de usuario e infraestructura de despliegue. El resultado es una herramienta que respeta la privacidad del usuario mientras entrega rendimiento de compresión de grado profesional.
Prueba OneImage Squoosh hoy, o explora nuestra extensión de navegador para acceso aún más rápido. Para desarrolladores construyendo herramientas similares, esperamos que esta inmersión técnica proporcione un plano útil.
---
Referencias y lectura adicional
- Google Squoosh (archivado) - Repositorio del proyecto original
- libimagequant-wasm - Port WebAssembly de pngquant
- pngquant - Biblioteca libimagequant original
- @jsquash - Colección de codecs de imagen WebAssembly
- Documentación WebAssembly - Mozilla Developer Network
- Web Workers API - Guía MDN
- Canvas API - Referencia de manipulación de imágenes
- Mejores prácticas de compresión de imágenes - Guía web.dev
- OneImage Squoosh - Prueba la herramienta
- Extensión de navegador OneImage - Versión de extensión de navegador
- Guía de formatos de imagen - Comparación integral de formatos
