Сжатие изображений — быстрый и надёжный способ ускорить сайт. Сегодня оптимальный набор форматов — это AVIF и WebP, при этом JPEG/PNG остаются «последней линией» совместимости. На практике всё упирается не только в конвертацию, но и в корректную отдачу по заголовку Accept, устойчивый fallback, минимизацию ошибок с кэшированием и MIME. Разберём полный практический стек: конверсия (cwebp/ImageMagick), Nginx map для детекции поддержки, try_files с fallback и грамотные заголовки кэширования.
Зачем WebP/AVIF в 2025 году
Оба формата дают существенную экономию трафика относительно JPEG/PNG при равном визуальном качестве, особенно на мобильных сетях. AVIF обычно выигрывает у WebP по коэффициенту сжатия на сложных сценах, но кодируется медленнее. WebP — компромисс: быстрее кодирование, широко поддерживается, прозрачность и анимация доступны, декодируется легко.
Ключ к безболезненной интеграции — не ломать URL-адреса, не заставлять фронтенд выбирать формат и не плодить тонны правил. Схема с Nginx map и try_files позволяет обслуживать разные форматы из одного и того же пути к изображению, опираясь на Accept и наличие предгенерированных файлов.
Предгенерация vs. конвертация «на лету»
Есть два подхода. Предгенерация (batch-конверсия) хороша предсказуемостью и стабильной задержкой отдачи: все варианты лежат на диске до запроса. Конвертация «на лету» гибче, но сложнее: очередь фоновых задач, защита от штормов, контроль CPU, прогрев кэша. В большинстве случаев достаточно предгенерации для статических каталогов изображений и периодических пересборок.
Если вы настраиваете Nginx под статику, удобнее делать это на управляемом сервере — например, на VDS, где полностью контролируете конфиги и ресурсы.
Надёжный паттерн хранения — «двойное расширение»: к .jpg или .png добавляем суффикс формата. Примеры: photo.jpg.webp и photo.jpg.avif. Это максимально упрощает логику Nginx: можно попробовать отдать $uri.avif, затем $uri.webp, и только потом исходник.
Конвертация в WebP: cwebp
Пакет cwebp из состава libwebp — стабильный и быстрый инструмент. Базовая команда для JPEG:
cwebp -q 82 -mt -m 6 -sharp_yuv -metadata icc,exif -o photo.jpg.webp photo.jpg
Пояснения к параметрам:
-q 82— типичный старт для фотографии, регулируйте по визуальному порогу.-mt— многопоточность.-m 6— уровень усилий кодера (6 — максимум в классической шкале).-sharp_yuv— более острый YUV-преобразователь, часто даёт лучшую детализацию.-metadata icc,exif— сохраняем профиль и EXIF при необходимости.
Для PNG с альфой:
cwebp -q 80 -alpha_q 85 -mt -m 6 -metadata icc -o logo.png.webp logo.png
Пакетная конверсия каталога:
find ./images -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) -print0 | xargs -0 -n1 -P4 -I{} sh -c 'cwebp -q 82 -mt -m 6 -sharp_yuv -metadata icc,exif -o "{}".webp "{}"'
Рекомендация: для фоток начать с q в диапазоне 78–86, для графики с альфой — 75–85 плюс -alpha_q подбирать отдельно. Если в исходных JPEG есть неправильная ориентация, сначала нормализуйте поворот (например, через ImageMagick -auto-orient), затем конвертируйте.
Конвертация в AVIF: avifenc и ImageMagick
AVIF даёт лучший битрейт при том же качестве, но сложнее и медленнее в кодировании. Инструменты: avifenc (libavif, кодек aom) и magick (ImageMagick 7 с поддержкой AVIF через libheif/libavif).
Примеры с avifenc для фотографий:
avifenc --min 20 --max 28 --speed 6 --jobs 4 --codec aom --depth 8 --yuv 420 photo.jpg photo.jpg.avif
Для изображений с прозрачностью и мелкими деталями:
avifenc --min 22 --max 30 --speed 6 --jobs 4 --codec aom --depth 8 --yuv 444 logo.png logo.png.avif
Чем ниже --min/--max — тем выше качество. --speed 6 — разумный компромисс; уменьшение до 4–5 даст плюс в качестве, но кодирование замедлится.
Через ImageMagick:
magick photo.jpg -auto-orient -define avif:codec=aom -define avif:speed=6 -quality 45 photo.jpg.avif
magick logo.png -auto-orient -define avif:codec=aom -define avif:speed=6 -quality 50 logo.png.avif
Шкала -quality у AVIF не эквивалентна JPEG/WebP. Начните с 40–55 и подгоняйте под контент. Для массовой конвертации используйте GNU Parallel или xargs с несколькими потоками, следите за загрузкой CPU и IO.
Организация файлов: простой вариант с «двойным расширением»
Храним photo.jpg, рядом photo.jpg.webp и photo.jpg.avif. Плюсы: не нужно переписывать пути в шаблонах; try_files становится тривиальным; MIME однозначен по последнему расширению. Минусы: занимает больше inode и места, но за счёт сильного сжатия окупается.
Nginx: определяем поддержку через Accept
Браузеры присылают в Accept приоритеты форматов, например image/avif,image/webp,*/*;q=0.8. Для наших задач достаточно проверить наличие подстроки: AVIF приоритетнее WebP, WebP — приоритетнее исходников. Сделаем это через map и соберём финальную переменную с желаемым суффиксом:
map $http_accept $img_avif {
default 0;
~*image/avif 1;
}
map $http_accept $img_webp {
default 0;
~*image/webp 1;
}
map "$img_avif$img_webp" $img_ext {
default "";
"10" ".avif";
"11" ".avif";
"01" ".webp";
"00" "";
}
Далее — одна локация для JPEG/PNG, где мы пытаемся отдать соответствующий вариант, а при его отсутствии — исходник. Вариант с «двойным расширением» получается очень компактным:
types {
image/avif avif;
image/webp webp;
}
location ~* \.(jpe?g|png)$ {
try_files $uri$img_ext $uri =404;
add_header Vary "Accept" always;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
Как это работает: если клиент поддерживает AVIF, Nginx сначала проверит $uri.avif; если файла нет — проверит $uri.webp (когда $img_ext указывает на WebP); если и этого нет — отдаст исходник. При отсутствии поддержки и AVIF, и WebP $img_ext пуст, и try_files сразу возьмёт оригинал.
Vary и CDN/прокси
Критично добавить Vary: Accept для избежания «перемешивания» вариантов в кэше промежуточных прокси/CDN. Без этого заголовка пользователь, не поддерживающий AVIF, может получить «битое» изображение, закешированное для другого клиента. Параметр always гарантирует заголовок даже при ответах 304.
Кэширование: стратегии и заголовки
Статическим изображениям обычно задаём «долгий» кэш в браузере и CDN. Простейшая и надёжная стратегия — Cache-Control: public, max-age=31536000, immutable. При изменении контента меняем имя файла (хеш в имени или версионирование каталога). Это лучше, чем договариваться об инвалидировании со множеством кэшей.
ETag и Last-Modified — опционально. Если изображения редко переупаковываются и вы используете файловые имена-версии, ETag не обязателен. Если включаете ETag — важно, чтобы он рассчитывался отдельно для каждого варианта (AVIF, WebP, исходник), иначе возможны конфликтные 304. В Nginx для статики по умолчанию выдаётся Last-Modified, этого достаточно в большинстве сценариев.
Для динамических бэкэндов уместны директивы stale-while-revalidate и stale-if-error (при работе через proxy_cache), но для чистой статики они не нужны. Также не стоит применять gzip/brotli к изображениям: они уже сжаты, вы только потратите CPU. Об оптимальном Brotli для текстовых ресурсов мы писали здесь: Brotli и OPCache на хостинге.

Правильные MIME-типы
Добавьте типы для AVIF и WebP, если их нет в вашем наборе MIME. Мы показали блок types выше. Проверьте через curl -I, что на .avif отдаётся Content-Type: image/avif, а на .webp — image/webp. Для двойного расширения (.jpg.avif) тип определяется последним расширением и проблем не возникает.
Тестирование: curl и отладочные заголовки
Имитируем браузер с поддержкой AVIF:
curl -I -H "Accept: image/avif,image/webp" https://example.com/images/photo.jpg
И браузер без современной поддержки:
curl -I -H "Accept: image/png,image/*;q=0.8,*/*;q=0.5" https://example.com/images/photo.jpg
Удобно добавить временный отладочный заголовок, чтобы видеть, какой вариант отдан:
add_header X-Image-Variant $img_ext always;
Не забудьте удалить его после запуска в проде.

Альтернативные схемы хранения и нюансы Nginx
Если хотите хранить конвертаты без «двойных расширений» (например, photo.avif рядом с photo.jpg), логика усложняется: нужно менять расширение в пути. Самый простой способ — не делать этого и придерживаться двойного расширения. Если всё же требуется использовать «чистые» .avif/.webp, придётся собирать кандидатный путь через map и try_files с подстановкой базового имени. Но это менее прозрачно, хуже отлаживается и труднее поддерживается.
Осторожнее с alias: при использовании alias вместо root меняются правила формирования $uri, и try_files может проверять не то, что вы ожидаете. Для простоты храните изображения под тем же root, где и сайт, или корректно перепроверьте пути.
Производительность и ресурсы: как не «поджечь» CPU
AVIF при хорошем качестве кодируется ощутимо дольше WebP. Для массовой переконвертации каталога с десятками тысяч файлов планируйте ночное окно и лимит потоков (-P в xargs или --jobs в avifenc). Имеет смысл разделить задачи: сначала WebP (быстро даёт значимый выигрыш), затем AVIF для ключевых изображений (герои, карточки товаров), а оставшиеся конвертировать постепенно.
От конвертации «на лету» откажитесь, если не можете гарантировать ограничение параллелизма и защиту от штормов кэш-миссов. Для большинства сайтов предгенерация под деплой — самый беспроблемный путь. Если управляете сервером через панель, посмотрите обзор: панели для VDS.
Частые ошибки и как их избежать
- Забыли
Vary: Accept. Симптом: в кэше оказываются «не ваши» варианты, пользователи получают битые изображения. Лечение: добавитьadd_header Vary "Accept" always;. - Неверный MIME. Симптом: Safari/Chrome ругаются или не отображают картинку. Лечение: добавить
types { image/avif avif; image/webp webp; }, проверитьcurl -I. - Сломанный путь в
try_files. Симптом: 404 для существующих вариантов. Лечение: используйте «двойное расширение» и простоеtry_files $uri$img_ext $uri =404;. - Чрезмерная компрессия. Симптом: артефакты, потеря читабельности. Лечение: поднять качество на 3–5 пунктов, для WebP с альфой отдельно поднять
-alpha_q, для AVIF играться c--min/--maxили-quality. - Проблемы с ориентацией. Симптом: повернутые фото. Лечение: нормализовать поворот перед конвертацией (
-auto-orientв ImageMagick), сохранить EXIF при необходимости. - Лишняя компрессия gzip/brotli. Симптом: бесполезная нагрузка CPU. Лечение: отключить сжатие для изображений, они уже оптимально упакованы.
SVG, GIF и прочие кейсы
SVG не стоит «прятать» за этой логикой: это вектор, он отлично кэшируется как есть. Анимированный GIF имеет смысл перекодировать в видео/анимацию WebP только при контролируемом фронтенде и тестах совместимости. Для единообразия в Nginx ограничьте локацию на jpg|jpeg|png, не задевая другие типы.
Мини-чеклист перед релизом
- Сгенерированы
.webpи.avifв нужных каталогах для ключевых изображений. - Nginx:
mapнастроены,try_filesпроверен под разнымиAccept. - Отдаём корректные
Content-Typeдля всех расширений. - Есть
Vary: Accept, заголовки кэша установлены и проверены. curl -Iпоказывает ожидаемые варианты и коды ответов.- CDN, если используется, учитывает
Varyи не переписывает заголовки.
Итог
Рецепт «без боли» складывается из четырёх частей: предсказуемая предгенерация (cwebp/avifenc или ImageMagick), простое хранение с «двойным расширением», лаконичная связка map + try_files в Nginx и аккуратные заголовки кэширования с Vary: Accept. Такой подход даёт максимальную отдачу от AVIF/WebP без изменений в шаблонах и без рисков сломать обратную совместимость. Настраивается один раз, работает стабильно, легко отлаживается и поддаётся автоматизации в любом конвейере сборки.


