Замеры сжатия изображений: десять образцов в браузере
Десять образцов, по пять измерений каждого, один браузер. Это измерения кодировщиков на том самом пути, который публикует сайт, включая случаи, где результат не в нашу пользу.
Что измерялось и как?
Десять случаев из семи файлов, по пять измерений на случай: бенчмарк измеряет время сжатия и размер результата. Фотография 12 MP кодируется в JPEG, WebP и AVIF, а скриншот 2 MP — в PNG и WebP.
Стенд идёт по публикуемому пути. Он импортирует src/lib/pool.js и src/lib/worker.js — тот же пул и тот же воркер, которые приложение использует для JPEG, PNG, WebP, AVIF и JXL. Для MotionPhoto он импортирует src/lib/motion-photo/pipeline.js — путь, которым приложение идёт для этого формата; MotionPhoto и в продакшене не проходят через пул воркеров. Здесь нет ни компрессора только для бенчмарка, ни второй реализации, которая могла бы разойтись с первой.
Каждый случай выполняется один раз как прогрев, затем пять раз с измерением. Случаи идут последовательно и не конкурируют за пул из четырёх воркеров. В таблице — медиана пяти замеров. p95 считается по ближайшему рангу: при пяти замерах это самый медленный, без интерполяции и без вида, что пять замеров описывают хвост распределения.
Браузер — headless Chrome 155 со свежим временным профилем на каждый запуск; расширения и фоновая сеть отключены. Страница отдаётся с localhost, отсчёт начинается после готовности пула и модулей WASM. Изоляция между источниками намеренно не включается: она заставила бы wasm-feature-detect сообщить о потоках и переключила бы oxipng на многопоточную сборку, а это не тот путь, что в продакшене.
Машина — Apple M4, 10 ядер, 16 ГБ памяти, macOS на arm64. Браузер сообщает о 10 аппаратных потоках; приложение ограничивает пул четырьмя воркерами.
Какие файлы тестировались?
Семь файлов, от JPEG 0,5 MP до фотографии 24 MP, плюс MotionPhoto 12 MP.
| Файл | Пиксели | Вход | Что представляет |
|---|---|---|---|
| photo-12mp.jpg | 4000×3000 | 4.92 MB | Детализированная фотография |
| shot-2mp.png | 1920×1080 | 1.36 MB | Скриншот интерфейса с резкими краями и полосами вместо текста |
| flat-1mp.png | 1000×1000 | 118.1 KB | Плоская графика с малым числом деталей |
| alpha-4mp.png | 2000×2000 | 8.26 MB | Изображение RGBA с настоящим альфа-каналом |
| large-24mp.jpg | 6000×4000 | 9.66 MB | Фотография 24 MP |
| tiny-0.5mp.jpg | 800×600 | 219.6 KB | Маленькое изображение; здесь заметна стоимость вызова |
| motion-12mp.jpg | Кадр 4000×3000 + видео 6 с 1280×720 | 9.98 MB | Синтетический MotionPhoto, собранный писателем из репозитория |
Все образцы сгенерированы ffmpeg из встроенных источников. SHA-256 каждого файла записан в bench/fixtures/manifest.json и проверяется до загрузки браузером: изменённый образец — это явная ошибка, а не тихо сдвинутая база.
Каковы результаты?
Десять случаев: отношения от 1,5% до 69,3% и медианы от 42,0 ms до 1 983,2 ms. Отношение — выход ÷ вход; меньше значит меньше. Медиана и p95 — настенное время всего случая, включая обмен с воркером. ms/MP — медиана, делённая на число мегапикселей.
Таблица и оба графика читают один файл, bench/published.json (id запуска, дата, окружение); при расхождении верен файл.
| Случай | Формат | MP | Вход | Выход | Отношение | Медиана | p95 | ms/MP |
|---|---|---|---|---|---|---|---|---|
| photo-12mp-jpg | JPEG → JPEG | 12 | 4.92 MB | 444.4 KB | 8.8% | 677.0 | 733.4 | 56.4 |
| photo-12mp-webp | JPEG → WebP | 12 | 4.92 MB | 325.2 KB | 6.5% | 997.5 | 1021.7 | 83.1 |
| photo-12mp-avif | JPEG → AVIF | 12 | 4.92 MB | 121.8 KB | 2.4% | 1983.2 | 2026.8 | 165.3 |
| shot-2mp-png | PNG → PNG | 2.07 | 1.36 MB | 600.6 KB | 43.2% | 613.1 | 618.1 | 296.2 |
| shot-2mp-webp | PNG → WebP | 2.07 | 1.36 MB | 21.0 KB | 1.5% | 151.3 | 156.1 | 73.1 |
| flat-1mp-png | PNG → PNG | 1 | 118.1 KB | 61.0 KB | 51.7% | 516.2 | 562.9 | 516.2 |
| alpha-4mp-avif | PNG → AVIF | 4 | 8.26 MB | 370.1 KB | 4.4% | 1551.7 | 1626.3 | 387.9 |
| large-24mp-jpg | JPEG → JPEG | 24 | 9.66 MB | 846.1 KB | 8.6% | 1228.1 | 1231.0 | 51.2 |
| tiny-0.5mp-webp | JPEG → WebP | 0.48 | 219.6 KB | 20.4 KB | 9.3% | 42.0 | 43.4 | 87.5 |
| motion-12mp | MotionPhoto | 12 | 9.98 MB | 6.92 MB | 69.3% | 1334.0 | 1377.3 | 111.2 |
Что говорят цифры?
На фотографии 12 MP AVIF в 3,65 раза меньше JPEG и в 2,93 раза медленнее; WebP между ними, а PNG — исключение без потерь с 296,2 ms/MP.
Насколько AVIF меньше JPEG?
Примерно треть: 2,4% против 8,8% на фотографии 12 MP, то есть 121,8 KB против 444,4 KB. WebP дал 325,2 KB. AVIF в 3,65 раза меньше JPEG и в 2,67 раза меньше WebP. Он же и самый медленный: медиана 1 983,2 ms, в 2,93 раза дольше JPEG и в 1,99 раза дольше WebP. На мегапиксель — 165,3 ms против 56,4 у JPEG и 83,1 у WebP.
Весь обмен в одной строке: чуть больше четверти байтов JPEG за тройное время кодирования. На пакете из сотен изображений это время реально; на одной главной картинке может быть неважно.
Насколько WebP меньше и медленнее JPEG?
На 26,8% меньше и в 1,47 раза медленнее: 325,2 KB против 444,4 KB и 997,5 ms против 677,0 ms на том же источнике 12 MP. Против AVIF он в 2,67 раза больше и в 1,99 раза быстрее. Он не лучший ни на одном краю; он середина в обе стороны.
Насколько быстр JPEG?
677,0 ms для фотографии 12 MP (56,4 ms/MP) и 1 228,1 ms для 24 MP (51,2 ms/MP) — самый быстрый из трёх кодеков здесь. Стоимость на мегапиксель почти ровная — так и выглядит быстрый блочный кодек. Его результат больше обоих современных форматов, но по времени он тот, кого нужно обогнать, и на этой машине никто не обогнал.
Почему PNG медленнее всех на мегапиксель?
296,2 ms/MP на скриншоте 2,07 MP — в 4,05 раза больше, чем 73,1 ms/MP у WebP, потому что оптимизация без потерь ищет фильтры и стратегии DEFLATE для каждого пикселя. Скриншот занял 613,1 ms в PNG и 151,3 ms в WebP. Плоский PNG 1 MP оказался хуже на пиксель: 516,2 ms, потому что фиксированная стоимость оптимизатора делится всего на один мегапиксель.
PNG — без потерь. Каждый пиксель сохраняется, а oxipng тратит время на поиск фильтров и стратегий DEFLATE для потока поменьше; поиск — это и есть смысл, и он же стоимость. Кодек с потерями может остановиться при достижении целевого качества. Оптимизатор без потерь должен установить, что удалять больше нечего, а число комбинаций растёт вместе с изображением.
Разница в размере так же пряма: скриншот весил 600,6 KB в PNG и 21,0 KB в WebP. Это не эквивалентные результаты — один без потерь, другой с потерями, — но они объясняют, почему PNG верен для схемы, которую вы будете править, и неверен для фотографии, которую вы публикуете.
Всегда ли более высокий уровень oxipng даёт меньший PNG?
Нет: уровень 3 был на 9,6–13,6% меньше на трёх скриншотах, но сэкономил всего четыре байта на фотографическом PNG 8,29 MP при 8,2-кратном времени. Уровень PNG измерялся отдельно через bench/png-levels.mjs, на тех же образцах, с одним прогревом и пятью прогонами на уровень. На трёх скриншотах (2,07, 8,29 и 15,73 MP) уровень 3 занял в 7,8–8,5 раза больше времени, чем уровень 1, а файлы вышли на 9,6–13,6% меньше. На фотографическом PNG 8,29 MP он дал на четыре байта меньше уровня 1 — 15 474 918 против 15 474 922 — и занял в 8,2 раза больше времени. На изображении RGBA он сэкономил 2,0% и занял в 7,1 раза больше времени. Дополнительный поиск почти ничего не покупает, когда изображение уже фотографическое.
Поэтому публикуемый воркер теперь по умолчанию использует уровень 2 и опускается до уровня 1 выше 4 MP. На 16 MP уровень 2 был на 1,0% больше уровня 1 и примерно вдвое медленнее, так что более высокий уровень не означает автоматически меньший файл. Цена изменения видна на плоских PNG с малым числом деталей: плоский образец 1 MP весит 62 501 B на уровне 2 против 48 138 B на уровне 3 — на 29,8% больше, — но кодируется за 437,9 ms вместо 2 028,3 ms, в 4,6 раза быстрее. Скриншоты платят меньше: при 2,07 MP файл уровня 2 на 7,2% больше уровня 3 и в 4,1 раза быстрее. Сайт выбрал более быстрый вариант по умолчанию и принимает эти байты.
Сжимает ли MotionPhoto видео?
Нет: перекодируется только кадр, а видео копируется байт в байт, поэтому синтетический файл 12 MP уменьшился на 30,7%, хотя половину его не трогали. MotionPhoto — это один файл: JPEG-кадр с добавленным видео и, на некоторых телефонах, gain map. Публикуемый путь декодирует и перекодирует кадр, затем собирает контейнер заново. Оно не декодируется, не перекодируется и не предлагается как цель. Gain map, если он есть, тоже копируется, а его абсолютное смещение в MPF переуказывается, чтобы HDR-показ продолжал работать.
Поэтому отношение для всего файла определяется тем, какую долю занимает видео. В синтетическом образце 12 MP кадр — это 5 156 418 B из 10 463 084 B входа (49,3%), видео — 5 306 666 B (50,7%); gain map нет. Кадр вышел 1 944 569 B, на 62,3% меньше, а весь файл — с 9,98 MB до 6,92 MB, на 30,7% меньше. Экономия кадра велика; экономия файла ограничена той половиной, которую не трогали.
Проверка на реальном образце использует MotionPhoto с OnePlus 13: кадр 7 140 978 B (44,7%), видео 7 968 544 B (49,9%), gain map 848 042 B (5,3%), всего 15 957 564 B. После пути «только кадр»: кадр 1 862 140 B (на 73,9% меньше), видео и gain map побайтно идентичны, всего 10 712 466 B — с 15,96 MB до 10,71 MB, на 32,9% меньше. Пересобранный файл был распознан как MotionPhoto и проигран на телефоне. Это путь gain map, проверенный на реальном железе, а не выведенный из синтетического образца.
Каковы ограничения?
Шесть оговорок, начиная с синтетических образцов и одной машины.
- Образцы сгенерированы ffmpeg из встроенных источников. Они воспроизводимы, но это не фотографии с камеры; другое изображение с другими деталями даст другое отношение.
- Образец MotionPhoto синтетический и без gain map. Его отношение для всего файла 69,3% — не 67,1% реального образца; разница в отсутствующем gain map и точном битрейте видео. Реальный образец указан отдельно.
- Эти цифры с одной машины, одного браузера и одного размера пула. Абсолютное время будет другим на другом железе, а на мобильных — сильнее.
- Цифры сравнимы только внутри одной среды. Вывод бенчмарка говорит это сам: локальный darwin-arm64 и CI ubuntu-latest — разные классы стендов.
- Это измерения кодировщиков, а не обещание про конкретный телефон. Они не включают состояние батареи, тепловой троттлинг, различия версий браузера и стоимость декодирования результата.
- Пиковая память не указана: для её измерения нужна изоляция между источниками, а она меняет путь oxipng. Бенчмарк пишет «n/a», а не число из другой сборки.
Как воспроизвести эти цифры?
Пятью командами, от npm ci до npm run bench.
npm ci
npm run build
npm run bench:fixtures
npm run bench:build
npm run bench
Запуск пишет JSON в bench/results/ и печатает таблицу выше в stdout. Сравнение уровней PNG — npm run bench:png-levels. Образцы сверяются с bench/fixtures/manifest.json до загрузки браузером; изменённый файл — жёсткая ошибка, а не тихая новая база.
Частые вопросы
Какой формат сжимает сильнее всего?
AVIF: на фотографии 12 MP он дал 121,8 KB, то есть 2,4% входа, против 6,5% у WebP и 8,8% у JPEG.
Какой формат кодирует быстрее всего?
JPEG: 677,0 ms для фотографии 12 MP против 997,5 ms у WebP и 1 983,2 ms у AVIF.
Почему PNG медленнее на мегапиксель?
Потому что он без потерь: oxipng ищет фильтры и стратегии DEFLATE для каждого пикселя. Скриншот 2,07 MP занял 296,2 ms/MP в PNG и 73,1 ms/MP в WebP — в 4,05 раза меньше.
Сжимает ли MotionPhoto видео?
Нет. Видео копируется байт в байт, перекодируется только кадр; в синтетическом образце 12 MP кадр уменьшился на 62,3%, а весь файл — на 30,7%, с 9,98 MB до 6,92 MB.
Что делают уровни 2 и 3 с размером PNG?
Уровень 3 меньше, но гораздо медленнее. Плоский PNG 1 MP весит 62 501 B на уровне 2 против 48 138 B на уровне 3 — на 29,8% больше, — но кодируется за 437,9 ms вместо 2 028,3 ms.
Сколько прогонов на каждый случай?
Один прогрев и пять измеренных прогонов; таблица показывает медиану этих пяти, а p95 — самый медленный замер.
Об этой статье
Опубликовано tinyimg. Стенд, генератор образцов и файлы результатов лежат в репозитории, а сырой JSON этого запуска пишется в bench/results/. Подпись — проект, а не человек: здесь заявляется не авторитет, а то, что метод записан и цифры можно воспроизвести.