OneImage
OneImage
·
technicalwebassemblyperformance

Construire un Squoosh amélioré : Compression d'images locale haute performance avec libimagequant-wasm

Construire un Squoosh amélioré : Compression d'images locale haute performance avec libimagequant-wasm

Une plongée approfondie dans la façon dont nous avons construit un outil de compression d'images haute performance et respectueux de la vie privée en combinant l'architecture de Google Squoosh avec libimagequant-wasm, Web Workers et des API web modernes.

Lorsque Google a annoncé la fermeture de Squoosh début 2023, la communauté des développeurs web a perdu un outil précieux pour la compression d'images côté client. Chez OneImage, nous avons vu une opportunité non seulement de préserver cette fonctionnalité, mais de l'améliorer. Cet article détaille l'approche technique que nous avons adoptée pour construire une version améliorée et prête pour la production de Squoosh qui privilégie la confidentialité, les performances et l'expérience développeur.

Comprendre l'architecture de Google Squoosh

Google Squoosh a été pionnier dans le concept d'exécution d'encodeurs d'images entièrement dans le navigateur en utilisant WebAssembly (WASM). L'architecture originale consistait en :

  • Traitement côté client : Toute la compression se fait localement, garantissant la confidentialité
  • Codecs WebAssembly : Encodeurs à vitesse native compilés en WASM (MozJPEG, OxiPNG, WebP, AVIF)
  • Web Workers : Décharger les calculs lourds pour éviter de bloquer l'interface utilisateur
  • Canvas API : Manipulation d'images et génération d'aperçus

Bien que révolutionnaire, Squoosh avait des limites :

  • La compression PNG reposait uniquement sur OxiPNG, qui privilégiait le taux de compression à la vitesse
  • Aucune capacité de traitement par lots intégrée
  • Configurations de préréglages limitées pour les cas d'usage courants
  • Logique d'interface utilisateur et de compression étroitement couplée

Notre stratégie d'amélioration

1. Intégration de libimagequant-wasm pour une compression PNG supérieure

La pierre angulaire de notre amélioration est libimagequant-wasm, un portage WebAssembly de la bibliothèque pngquant standard de l'industrie. Cette bibliothèque utilise un algorithme sophistiqué de quantification des couleurs qui produit des résultats visuellement supérieurs par rapport à la réduction de palette simple.

Pourquoi libimagequant ?

  • Qualité perceptuelle : Utilise un algorithme de coupe médiane modifié optimisé pour la perception humaine
  • Palettes adaptatives : Génère des palettes optimales de 2 à 256 couleurs basées sur le contenu de l'image
  • Gestion de la transparence : Préserve les canaux alpha pendant la compression
  • Performance : Fonctionne à une vitesse proche du natif grâce à WASM

Détails de mise en œuvre

Voici comment nous avons intégré libimagequant dans notre pipeline de compression :

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 });
  
  // Mapper le niveau de compression (0-10) au nombre de couleurs (256-2)
  const maxColors = Math.max(2, 256 - (25.6 * level));
  
  const quantized = await quantizer.quantizeImageData(imageData, {
    maxColors: Math.floor(maxColors),
    speed: 1,          // Équilibre entre qualité et vitesse
    quality: {
      min: 0,
      target: 100      // Viser la qualité la plus élevée dans la limite de couleurs
    }
  });
  
  return new Uint8Array(quantized.pngBytes);
}

Paramètres clés expliqués :

  • maxColors : Contrôle la taille de la palette. Moins de couleurs = fichier plus petit, mais qualité potentiellement pire
  • speed : Plage 1-10, où 1 est le plus lent mais la qualité la plus élevée
  • quality.target : Définit le seuil de qualité cible (0-100)

Notre outil Squoosh utilise cette implémentation pour offrir des taux de compression de 60 à 80 % avec une perte perceptuelle minimale.

2. Architecture d'un système Web Worker robuste

Pour empêcher le navigateur de geler pendant la compression (en particulier pour les grandes images ou les opérations par lots), nous avons construit une architecture Web Worker dédiée :

// 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 : 'Erreur inconnue',
      });
    }
  }
};

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);
  }
}

Avantages du Worker :

  • Interface utilisateur non bloquante pendant la compression
  • Capacité d'annuler les opérations de longue durée
  • Traitement parallèle pour les opérations par lots (plusieurs workers)
  • Isolation de la mémoire empêchant les fuites mémoire du thread principal

3. Création de préréglages de compression intelligents

Plutôt que d'exposer des paramètres d'encodeur bruts, nous avons créé trois préréglages optimisés pour les cas d'usage courants :

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

Ces préréglages ont été calibrés grâce à des tests approfondis sur divers types d'images (photos, illustrations, captures d'écran, éléments d'interface utilisateur) pour trouver des compromis qualité-taille optimaux.

4. Mise en œuvre du traitement par lots efficace

Pour les utilisateurs compressant plusieurs images, nous avons construit un système de file d'attente avec suivi de la progression :

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));
      }
    }
  }
}

Cette approche :

  • Maximise l'utilisation du CPU en exécutant des workers parallèles
  • Prévient les plantages du navigateur en limitant la concurrence
  • Fournit des mises à jour de progression en temps réel aux utilisateurs

Essayez notre traitement par lots en action sur OneImage Squoosh.

Optimisations de performance

Gestion de la mémoire

Les grandes images peuvent rapidement épuiser la mémoire du navigateur. Nous avons implémenté plusieurs stratégies d'atténuation :

async function processLargeImage(file: File): Promise<CompressResult> {
  const MAX_DIMENSION = 4096;
  const img = await loadImage(file);
  
  // Réduire si nécessaire
  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);
  }
  
  // Utiliser OffscreenCanvas quand disponible pour une meilleure gestion de la mémoire
  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);
  
  // Libérer immédiatement les ressources canvas et image
  URL.revokeObjectURL(img.src);
  
  return await compress(imageData, options);
}

Mise en cache des modules WASM

Les modules WebAssembly bénéficient d'une mise en cache agressive :

let cachedWasmModule: typeof wasmModule | null = null;

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

Cela réduit le temps d'initialisation de ~500 ms à quasi-instantané lors des compressions suivantes.

Extension de la boîte à outils

Bien que Squoosh se concentre sur la compression, nous avons construit une suite complète d'outils complémentaires :

Tous les outils partagent les mêmes principes architecturaux : respect de la vie privée d'abord, alimentés par WASM et entièrement côté client.

Intégration d'extension de navigateur

Nous avons étendu l'architecture de l'application web dans une extension de navigateur, permettant :

  • Accès instantané via la popup de la barre d'outils
  • Intégration du menu contextuel pour la compression par clic droit
  • Gestion d'état basée sur les onglets
  • Stockage local pour les préférences de préréglages

L'extension réutilise la même infrastructure Web Worker et WASM, garantissant un comportement cohérent sur toutes les plateformes.

Déploiement et infrastructure

Edge Computing avec Cloudflare

Nous déployons OneImage Squoosh sur Cloudflare Pages, exploitant :

  • CDN mondial pour des temps de chargement initiaux <50 ms dans le monde entier
  • Compression HTTP/3 et Brotli pour les ressources
  • En-têtes de cache intelligents pour les modules WASM
  • Zéro démarrage à froid (ressources statiques uniquement)

Optimisation de la construction

Notre configuration Next.js inclut :

// next.config.ts
module.exports = {
  webpack: (config, { isServer }) => {
    // Support des fichiers .wasm
    config.experiments = {
      asyncWebAssembly: true,
      layers: true,
    };
    
    // Optimiser les imports de workers
    config.module.rules.push({
      test: /\.worker\.(ts|js)$/,
      use: { loader: 'worker-loader' }
    });
    
    return config;
  },
  
  // Fractionnement de code agressif
  experimental: {
    optimizePackageImports: [
      '@jsquash/jpeg',
      '@jsquash/png',
      '@jsquash/webp',
      '@jsquash/avif'
    ]
  }
};

Cela garantit que les encodeurs sont chargés à la demande, gardant le bundle initial sous 100 Ko (gzippé).

Tests et assurance qualité

Nous maintenons une couverture de test complète pour la logique de compression :

// __tests__/advanced-compressor.test.ts
describe('AdvancedImageCompressor', () => {
  it('devrait compresser PNG avec 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('devrait gérer le traitement par lots avec concurrence', 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;
    
    // Devrait être plus rapide que le traitement séquentiel
    expect(duration).toBeLessThan(10 * 1000); // <1s par image
  });
});

Leçons apprises

  1. WASM est prêt pour la production : Avec un chargement et une mise en cache de modules appropriés, les performances WASM rivalisent avec les applications natives
  2. Les Web Workers sont essentiels : Pour toute tâche gourmande en CPU, le déchargement vers des workers est non négociable
  3. Préréglages utilisateur > contrôles bruts : La plupart des utilisateurs préfèrent de "bons défauts" à un réglage granulaire
  4. La mémoire compte : Profilez toujours l'utilisation de la mémoire sur les grandes images et implémentez des garde-fous
  5. La confidentialité se vend : Mettre l'accent sur "aucun téléversement serveur" résonne fortement avec les utilisateurs

Open Source et communauté

Bien que OneImage Squoosh soit un produit commercial, nous contribuons à l'écosystème :

  • Rapports de bugs et PRs aux mainteneurs @jsquash
  • Améliorations de la documentation pour libimagequant-wasm
  • Partage de benchmarks de performance et de meilleures pratiques

Conclusion

Construire un Squoosh amélioré a nécessité plus que la simple intégration de libimagequant-wasm. Cela a demandé des décisions architecturales minutieuses autour des Web Workers, de la gestion de la mémoire, de l'expérience utilisateur et de l'infrastructure de déploiement. Le résultat est un outil qui respecte la vie privée des utilisateurs tout en offrant des performances de compression de qualité professionnelle.

Essayez OneImage Squoosh aujourd'hui, ou explorez notre extension de navigateur pour un accès encore plus rapide. Pour les développeurs construisant des outils similaires, nous espérons que cette plongée technique fournit un blueprint utile.

---

Références et lectures complémentaires