Когда 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с/изображение
});
});
Извлеченные уроки
- WASM готов к продакшену: С правильной загрузкой модулей и кэшированием производительность WASM конкурирует с нативными приложениями
- Web Workers необходимы: Для CPU-интенсивных задач разгрузка воркерам не подлежит обсуждению
- Пресеты пользователей > Сырой контроль: Большинство пользователей предпочитают "хорошие значения по умолчанию" тонкой настройке
- Память имеет значение: Всегда профилируйте использование памяти на больших изображениях и реализуйте защитные механизмы
- Конфиденциальность продается: Подчеркивание "никаких загрузок на сервер" сильно резонирует с пользователями
Открытый исходный код и сообщество
Хотя OneImage Squoosh является коммерческим продуктом, мы вносим вклад в экосистему:
- Отчеты об ошибках и PR мейнтейнерам @jsquash
- Улучшения документации для libimagequant-wasm
- Публикация бенчмарков производительности и лучших практик
Заключение
Создание улучшенного Squoosh требовало больше, чем просто интеграцию libimagequant-wasm. Это требовало продуманных архитектурных решений по Web Workers, управлению памятью, пользовательскому опыту и инфраструктуре развертывания. Результат — инструмент, который обеспечивает профессиональное качество сжатия, уважая конфиденциальность пользователей.
Попробуйте OneImage Squoosh сегодня или изучите браузерное расширение для более быстрого доступа. Для разработчиков, создающих похожие инструменты, мы надеемся, что это техническое погружение предоставило полезный план.
---
Ссылки и дополнительные ресурсы
- Google Squoosh (Архив) - Репозиторий оригинального проекта
- libimagequant-wasm - WebAssembly порт pngquant
- pngquant - Оригинальная библиотека libimagequant
- @jsquash - Коллекция WebAssembly кодеков изображений
- Документация WebAssembly - Mozilla Developer Network
- Web Workers API - Руководство MDN
- Canvas API - Справочник по манипуляциям с изображениями
- Лучшие практики сжатия изображений - Руководство web.dev
- OneImage Squoosh - Попробуйте инструмент
- Браузерное расширение OneImage - Версия браузерного расширения
- Руководство по форматам изображений - Всестороннее сравнение форматов
