OneImage
OneImage
·
technicalwebassemblyperformance

Построение улучшенного Squoosh: Высокопроизводительное локальное сжатие изображений с libimagequant-wasm

Построение улучшенного Squoosh: Высокопроизводительное локальное сжатие изображений с libimagequant-wasm

Глубокое погружение в то, как мы объединили архитектуру Google Squoosh с libimagequant-wasm, Web Workers и современными веб-API для создания инструмента сжатия изображений, ориентированного на конфиденциальность и высокую производительность.

Когда Google объявил о закрытии Squoosh в начале 2023 года, сообщество веб-разработчиков потеряло ценный инструмент для клиентского сжатия изображений. В OneImage мы увидели возможность не только сохранить эту функциональность, но и улучшить ее. Эта статья подробно описывает технический подход, который мы использовали для создания готовой к продакшену улучшенной версии Squoosh, которая приоритизирует конфиденциальность, производительность и опыт разработчика.

Понимание архитектуры Google Squoosh

Google Squoosh был пионером концепции использования WebAssembly (WASM) для полного запуска кодировщиков изображений в браузере. Оригинальная архитектура состояла из:

  • Клиентская обработка: Все сжатие происходило локально, обеспечивая конфиденциальность
  • WebAssembly кодеки: Кодировщики на нативной скорости (MozJPEG, OxiPNG, WebP, AVIF), скомпилированные в WASM
  • Web Workers: Разгрузка тяжелых вычислений для предотвращения блокировки UI
  • Canvas API: Манипуляции с изображениями и генерация превью

Хотя это было новаторским, у Squoosh были ограничения:

  • Сжатие PNG полагалось только на OxiPNG, который приоритизировал степень сжатия над скоростью
  • Отсутствие встроенной пакетной обработки
  • Ограниченные пресетные конфигурации для обычных случаев использования
  • Тесная связь UI и логики сжатия

Наша стратегия улучшения

1. Интеграция libimagequant-wasm для превосходного сжатия PNG

Ядром нашего улучшения была интеграция libimagequant-wasm, порта WebAssembly отраслевого стандарта библиотеки pngquant. Эта библиотека использует сложные алгоритмы квантования цветов, которые производят визуально превосходные результаты по сравнению с простым сокращением палитры.

Почему libimagequant?

  • Перцептивное качество: Использует модифицированный алгоритм медианного разреза, оптимизированный для человеческого восприятия
  • Адаптивная палитра: Генерирует оптимальную палитру от 2 до 256 цветов на основе содержимого изображения
  • Обработка прозрачности: Сохраняет альфа-каналы при сжатии
  • Производительность: Работает почти на нативной скорости благодаря WASM

Детали реализации

Вот как мы интегрировали libimagequant в наш конвейер сжатия:

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 });
  
  // Сопоставление уровня сжатия (0-10) с количеством цветов (256-2)
  const maxColors = Math.max(2, 256 - (25.6 * level));
  
  const quantized = await quantizer.quantizeImageData(imageData, {
    maxColors: Math.floor(maxColors),
    speed: 1,          // Баланс качества и скорости
    quality: {
      min: 0,
      target: 100      // Стремление к максимальному качеству в пределах ограничения цветов
    }
  });
  
  return new Uint8Array(quantized.pngBytes);
}

Ключевые объяснения параметров:

  • maxColors: Контролирует размер палитры. Меньше цветов = меньший файл, но потенциально худшее качество
  • speed: Диапазон 1-10, 1 самый медленный, но лучшее качество
  • quality.target: Устанавливает порог целевого качества (0-100)

Наш инструмент Squoosh использует эту реализацию для достижения степени сжатия 60-80% с минимальной перцептивной потерей.

2. Разработка надежной системы Web Worker

Чтобы предотвратить зависание браузера во время сжатия (особенно для больших изображений или пакетных операций), мы построили выделенную архитектуру Web Worker:

// 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 : 'Неизвестная ошибка',
      });
    }
  }
};

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

Преимущества Worker:

  • Неблокирующий UI во время сжатия
  • Возможность отменить долгосрочные задачи
  • Параллельная обработка для пакетных операций (несколько воркеров)
  • Изоляция памяти предотвращает утечки памяти основного потока

3. Создание умных пресетов сжатия

Вместо раскрытия сырых параметров кодировщика мы создали три пресета, оптимизированных для обычных случаев использования:

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

Эти пресеты были откалиброваны через обширное тестирование на различных типах изображений (фотографии, иллюстрации, скриншоты, UI-элементы) для нахождения оптимальных компромиссов качество-размер.

4. Реализация эффективной пакетной обработки

Для пользователей, сжимающих несколько изображений, мы построили систему очередей с отслеживанием прогресса:

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

Этот подход:

  • Максимизирует использование CPU запуском параллельных воркеров
  • Предотвращает сбои браузера ограничением одновременности
  • Предоставляет обновления прогресса в реальном времени пользователям

Попробуйте пакетную обработку в действии на OneImage Squoosh.

Оптимизации производительности

Управление памятью

Большие изображения могут быстро исчерпать память браузера. Мы реализовали несколько стратегий смягчения:

async function processLargeImage(file: File): Promise<CompressResult> {
  const MAX_DIMENSION = 4096;
  const img = await loadImage(file);
  
  // Уменьшение размера при необходимости
  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);
  }
  
  // Использование OffscreenCanvas где возможно для лучшей обработки памяти
  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);
  
  // Немедленное освобождение ресурсов canvas и изображения
  URL.revokeObjectURL(img.src);
  
  return await compress(imageData, options);
}

Кэширование WASM-модулей

WebAssembly модули выигрывают от агрессивного кэширования:

let cachedWasmModule: typeof wasmModule | null = null;

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

Это сокращает время инициализации с ~500мс до почти мгновенного при последующих сжатиях.

Расширение набора инструментов

Хотя Squoosh фокусируется на сжатии, мы построили полный набор дополнительных инструментов:

  • Image Resize: Умное изменение размера с несколькими алгоритмами
  • EXIF Remover: Удаление метаданных для конфиденциальности
  • Image Overlay: Водяные знаки и брендинг
  • Image Blur: Редактирование чувствительного контента
  • Crop Tool: Точная обрезка по соотношению сторон

Все инструменты разделяют одни и те же архитектурные принципы: конфиденциальность прежде всего, на основе WASM, полностью клиентские.

Интеграция браузерного расширения

Мы расширили архитектуру веб-приложения в браузерное расширение, обеспечивая:

  • Мгновенный доступ через всплывающее окно панели инструментов
  • Интеграция контекстного меню для сжатия правой кнопкой мыши
  • Управление состоянием на основе вкладок
  • Локальное хранилище для пресетных предпочтений

Расширение повторно использует ту же инфраструктуру Web Worker и WASM, обеспечивая согласованное поведение на всех платформах.

Развертывание и инфраструктура

Граничные вычисления с Cloudflare

Мы развернули OneImage Squoosh на Cloudflare Pages, используя:

  • Глобальную CDN для времени начальной загрузки <50мс по всему миру
  • HTTP/3 и Brotli сжатие для активов
  • Умные заголовки кэширования для WASM-модулей
  • Отсутствие холодных стартов (только статические активы)

Оптимизации сборки

Наша конфигурация Next.js включает:

// next.config.ts
module.exports = {
  webpack: (config, { isServer }) => {
    // Поддержка файлов .wasm
    config.experiments = {
      asyncWebAssembly: true,
      layers: true,
    };
    
    // Оптимизация импорта воркеров
    config.module.rules.push({
      test: /\.worker\.(ts|js)$/,
      use: { loader: 'worker-loader' }
    });
    
    return config;
  },
  
  // Агрессивное разделение кода
  experimental: {
    optimizePackageImports: [
      '@jsquash/jpeg',
      '@jsquash/png',
      '@jsquash/webp',
      '@jsquash/avif'
    ]
  }
};

Это обеспечивает загрузку кодировщиков по требованию, сохраняя начальный бандл ниже 100КБ (gzipped).

Тестирование и обеспечение качества

Мы поддерживаем всестороннее тестовое покрытие для логики сжатия:

// __tests__/advanced-compressor.test.ts
describe('AdvancedImageCompressor', () => {
  it('должен сжимать PNG с помощью 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('должен обрабатывать пакетную обработку с одновременностью', 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;
    
    // Должно быть быстрее последовательной обработки
    expect(duration).toBeLessThan(10 * 1000); // <1с/изображение
  });
});

Извлеченные уроки

  1. WASM готов к продакшену: С правильной загрузкой модулей и кэшированием производительность WASM конкурирует с нативными приложениями
  2. Web Workers необходимы: Для CPU-интенсивных задач разгрузка воркерам не подлежит обсуждению
  3. Пресеты пользователей > Сырой контроль: Большинство пользователей предпочитают "хорошие значения по умолчанию" тонкой настройке
  4. Память имеет значение: Всегда профилируйте использование памяти на больших изображениях и реализуйте защитные механизмы
  5. Конфиденциальность продается: Подчеркивание "никаких загрузок на сервер" сильно резонирует с пользователями

Открытый исходный код и сообщество

Хотя OneImage Squoosh является коммерческим продуктом, мы вносим вклад в экосистему:

  • Отчеты об ошибках и PR мейнтейнерам @jsquash
  • Улучшения документации для libimagequant-wasm
  • Публикация бенчмарков производительности и лучших практик

Заключение

Создание улучшенного Squoosh требовало больше, чем просто интеграцию libimagequant-wasm. Это требовало продуманных архитектурных решений по Web Workers, управлению памятью, пользовательскому опыту и инфраструктуре развертывания. Результат — инструмент, который обеспечивает профессиональное качество сжатия, уважая конфиденциальность пользователей.

Попробуйте OneImage Squoosh сегодня или изучите браузерное расширение для более быстрого доступа. Для разработчиков, создающих похожие инструменты, мы надеемся, что это техническое погружение предоставило полезный план.

---

Ссылки и дополнительные ресурсы