Оптимизация хранения и отдачи изображений: форматы, сжатие и автоматическая конвертация | AdminWiki

Оптимизация хранения и отдачи изображений: форматы, сжатие и автоматическая конвертация

11 сентября 2026 12 мин. чтения

Почему оптимизация изображений критична для DevOps

Прямой ответ: держите исходники в одном качественном формате, один раз генерируйте WebP и AVIF рядом с оригиналом, отдавайте клиенту тот вариант, который он поддерживает по заголовку Accept, и обязательно кэшируйте результат. Такая схема сокращает объём хранилища на 30-70% и снимает основную нагрузку с CPU, потому что один и тот же файл не конвертируется при каждом запросе.

Изображения занимают 50-70% веса типовой страницы. Каталог из 200 000 товарных фото по 400 КБ в JPEG занимает около 76 ГБ на диске, и примерно столько же уходит на резервные копии и на кэш в CDN. Те же файлы в WebP дают около 50 ГБ, в AVIF около 38 ГБ. Скидка 40% на бэкапах такого объёма освобождает десятки гигабайт без единой правки контента.

Трафик считается так же быстро. Показ изображения на 400 КБ миллион раз в месяц расходует 400 ГБ канала, версия в AVIF на 180 КБ укладывается в 180 ГБ. Плюс браузер быстрее отрисовывает первый экран, а сервер меньше времени тратит на передачу данных.

Ручная перегонка файлов в графическом редакторе не масштабируется на каталог в десятки тысяч позиций. Дальше речь о том, что настраивает DevOps: сравнение JPEG, WebP, AVIF, HEIC и PNG, подбор качества сжатия, раздача вариантов через Nginx, imgproxy и Thumbor, генерация превью и адаптивных версий, а также шесть ошибок, которые чаще всего ломают продакшен.

Выбор формата изображений: JPEG, WebP, AVIF, HEIC

JPEG остаётся форматом совместимости: его понимают все браузеры и почти все мобильные клиенты. WebP даёт выигрыш 25-35% при сопоставимом визуальном качестве и поддерживается всеми актуальными браузерами. AVIF экономит ещё больше, 40-50% относительно JPEG, но требует более свежих версий браузеров и заметно больше процессорного времени при кодировании. HEIC удобен для съёмки на iPhone, для веба он не подходит: вне Safari поддержки почти нет, файлы приходится конвертировать. PNG держит точную графику и прозрачность, зато на фотографиях проигрывает по размеру в 3-10 раз.

Сравнительная таблица форматов

ФорматТип сжатияПрозрачностьАнимацияПоддержка браузерамиЭкономия относительно JPEG
JPEGс потеряминетнетвсебазовая линия
PNGбез потерь, есть квантование палитрыда, альфа-каналда, APNGвседля фото размер больше в 3-10 раз
WebPс потерями и без потерьдадаChrome 32+, Firefox 65+, Safari 14+25-35%
AVIFс потерями и без потерьдадаChrome 85+, Firefox 93+, Safari 16.1+40-50%
HEIC/HEIFс потерями и без потерьдада, последовательностьSafari на платформах Apple, вне Apple почти нет30-50%, в вебе не используется
SVGвектор, без потерьдада, SMIL и CSSвседля иконок меньше в разы

Версии в таблице минимальные для стабильной работы. Перед запуском посмотрите логи User-Agent: если 2-3% посетителей сидят на старых клиентах, им нужен fallback в JPEG, иначе часть каталога останется без картинок.

Когда какой формат выбирать

  • Фотографии, баннеры, обложки: AVIF как основной вариант, WebP и JPEG в fallback. На каталоге из 200 000 файлов экономия доходит до 30 ГБ.
  • Иконки, логотипы, простые схемы: SVG. Он масштабируется без потерь и весит единицы килобайт, растровые копии нужны только для писем и старых клиентов.
  • Скриншоты интерфейса и изображения с текстом: PNG с квантованием палитры или WebP без потерь. Сжатие с потерями даёт мыло вокруг букв.
  • Анимация: WebP или AVIF вместо GIF. Анимированный WebP легче GIF в 3-10 раз, но часть старых клиентов его не покажет.
  • Архив исходников: PNG, TIFF или JPEG q95+. Из мастер-файла генерируются все производные.

При автоматической конвертации выбирать один формат не нужно. Держите исходник в PNG или JPEG, а сервис обработки либо nginx отдаст лучший из поддерживаемых клиентом вариантов.

Настройка сжатия без потери качества

Сжатие с потерями убирает детали, которые глаз почти не различает, и уменьшает файл в разы. Сжатие без потерь сохраняет пиксели бит в бит, но на фотографиях экономит 5-15%. Практическое правило: фотографии сжимайте с потерями, графику с текстом, скриншоты интерфейсов и логотипы - без потерь или с качеством выше 90.

Границы качества: JPEG 80-85, WebP 75-80, AVIF 60-70. У JPEG ниже 70 появляются блочные артефакты на градиентах неба и ореолы вокруг шрифта. У WebP критическая зона начинается около 60, у AVIF около 45, но AVIF кодируется медленно, поэтому в пайплайне генерации ставьте speed 4-6, а не максимальное качество любой ценой.

Практические рекомендации по параметрам

ФорматКачествоЭкономия против JPEG q90Инструмент
JPEG80-85, прогрессивный20-30%mozjpeg (cjpeg), jpegoptim
WebP75-8025-35%cwebp
AVIF60-70, cq-level 20-3040-50%avifenc, libaom
PNGбез потерь или квантование40-70% на скриншотахoxipng, pngquant
cwebp -q 80 -m 6 -metadata icc input.jpg -o output.webp
avifenc --min 10 --max 30 --speed 4 input.png output.avif
cjpeg -quality 82 -progressive -optimize input.ppm > output.jpg
jpegoptim --all-progressive -m85 *.jpg
pngquant --quality=70-85 --speed 1 --strip input.png
oxipng -o 4 --strip safe input.png

Проверяйте результат метриками, а не только глазами на одном файле. SSIM и PSNR считаются быстро и подходят для автоматического контроля в пайплайне: порог SSIM 0.98 к исходнику отсекает брак. Метрика Butteraugli ближе к восприятию, но дороже по времени. Возьмите 10-15 типовых изображений (портрет, пейзаж, текст, товар на белом фоне) и сравните их попарно в слайдере.

Два предупреждения. Не сжимайте повторно: перекодирование готового WebP в JPEG или генерация второго поколения AVIF копит артефакты, поэтому производные всегда строятся от мастер-файла. И не прогоняйте уже сжатые изображения через gzip или brotli, это тратит CPU без выигрыша; как проверить корректность сжатия текстовых ресурсов, разобрано в статье про анализ gzip-сжатия.

Автоматическая конвертация на лету: Nginx, imgproxy, Thumbor

Подходов два. Первый: сгенерировать WebP и AVIF заранее, при загрузке файла или на этапе сборки, и раздавать их как статику. Второй: конвертировать на лету отдельным сервисом. Для каталогов в сотни тысяч файлов первый дешевле по CPU, второй гибче: любой размер и кроп доступны по URL без хранения всех вариантов.

Nginx: конвертация в WebP и AVIF

Штатный модуль ngx_http_image_filter_module умеет ресайз, кроп и поворот JPEG, PNG и GIF, но не отдаёт WebP и AVIF. Для этих форматов нужны сторонние модули или внешний сервис, поэтому nginx обычно работает как раздающий слой с выбором варианта по заголовку Accept.

map $http_accept $img_suffix {
    default        "";
    '~*image/avif' '.avif';
    '~*image/webp' '.webp';
}

server {
    location ~* ^/images/(.+)\.(jpe?g|png)$ {
        add_header Vary Accept;
        try_files /images/$1$img_suffix $uri =404;
    }
}

Директиву map держат в контексте http. Порядок строк важен: map берёт первое совпадение, поэтому AVIF проверяется раньше WebP, ведь браузеры с поддержкой AVIF обычно перечисляют и image/webp.

Что проверить перед выкаткой. В mime.types должны быть строки image/webp и image/avif, иначе nginx отдаст файл как application/octet-stream. Заголовок Vary: Accept обязателен: без него промежуточный кэш или CDN однажды отдаст AVIF клиенту, который его не понимает. Учтите, что add_header не наследуется в location, где объявлен свой add_header, поэтому строки кэширования придётся продублировать. Готовые блоки location с Cache-Control, immutable и stale-while-revalidate для картинок собраны в статье про кеширование статических файлов в Nginx.

imgproxy: быстрый старт и настройка

imgproxy - отдельный сервис на Go, обработка идёт через libvips. Он читает файлы из S3-совместимого хранилища, по локальному пути или по внешнему URL, отдаёт JPEG, PNG, WebP, AVIF и GIF, умеет умный кроп и подпись ссылок.

docker run -d --name imgproxy \
  -p 8080:8080 \
  -e IMGPROXY_KEY=736563726574 \
  -e IMGPROXY_SALT=686561727473 \
  -e IMGPROXY_ENABLE_WEBP_DETECTION=true \
  -e IMGPROXY_ENABLE_AVIF_DETECTION=true \
  -e IMGPROXY_CACHE_SIZE=536870912 \
  -e IMGPROXY_TTL=31536000 \
  darthsim/imgproxy:v3

Адрес строится по схеме /{подпись}/{опции}/{гравитация}/{источник}@{расширение}. Режим /insecure/ разрешён по умолчанию и годится только для локальных тестов: он позволяет любому собрать ссылку на ресайз, поэтому в продакшене задайте IMGPROXY_KEY и IMGPROXY_SALT (шестнадцатеричные строки) и подписывайте URL.

# локальный тест, без подписи
http://localhost:8080/insecure/rs:fill:600:400/plain/s3://bucket/photo.jpg@avif

# продакшен, подпись в первом сегменте
http://img.example.com/K1y2Abc.../rs:fill:600:400/plain/s3://bucket/photo.jpg@webp

Детекция формата включается переменными IMGPROXY_ENABLE_WEBP_DETECTION и IMGPROXY_ENABLE_AVIF_DETECTION в версиях 3.x: imgproxy сам читает Accept, подставляет формат и добавляет в ответ Vary: Accept. Кэш задаётся IMGPROXY_CACHE_SIZE (размер в байтах, 536870912 это 512 МБ) и IMGPROXY_TTL. Локальный кэш живёт в памяти одного процесса, поэтому при нескольких репликах выносите его на общий диск, в S3 или на CDN, иначе каждый под будет конвертировать одно и то же заново. Метрики доступны на /metrics в формате Prometheus, там видно время обработки по форматам.

Развернуть imgproxy можно в контейнере на VPS или в managed Kubernetes: например, в Timeweb Cloud есть серверы, Kubernetes и объектное хранилище под исходники, что закрывает всю схему без собственного железа.

Thumbor: конфигурация и особенности

Thumbor написан на Python и работает через Pillow или libvips. Его сильная сторона - фильтры и умный кроп на OpenCV, слабая - расход памяти и число зависимостей.

pip install thumbor
thumbor-config > thumbor.conf
thumbor --port=8888 --conf=thumbor.conf
QUALITY = 80
WEBP_QUALITY = 80
AUTO_WEBP = True
# AUTO_AVIF = True  # Thumbor 7 и новее при сборке с поддержкой AVIF
SECURITY_KEY = 'длинная-случайная-строка'
ALLOW_UNSAFE_URLS = False
RESULT_STORAGE = 'thumbor.result_storages.file_storage'
RESULT_STORAGE_FILE_STORAGE_ROOT_PATH = '/var/lib/thumbor/result_storage'

URL-схема: /{подпись}/{ширина}x{высота}/{режим}/{путь}. Подпись считается как HMAC-SHA1 от пути с SECURITY_KEY, поэтому в продакшене ALLOW_UNSAFE_URLS выключают, а /unsafe/ оставляют для локальных прогонов.

# локальный тест: умный кроп 300x300
http://localhost:8888/unsafe/300x300/smart/images/photo.jpg

# продакшен: подпись и формат
http://thumbor.example.com/AbCdEf.../600x0/filters:format(webp)/images/photo.jpg

Что учесть. AUTO_WEBP включает отдачу WebP по Accept. Поддержка AVIF появилась в Thumbor 7 и зависит от сборки: проверяйте свой стенд тестовым запросом с filters:format(avif), а не только номером версии. Результаты конвертации складывайте в RESULT_STORAGE (файлы, Redis, Memcached): без него каждый запрос уходит в Pillow и выедает ядро целиком. Планируйте лимиты памяти: Python-воркер с большим изображением легко занимает сотни мегабайт.

Генерация превью и адаптивных версий

Смысл превью в том, чтобы не гонять оригинал на 3000 пикселей на экран шириной 390. Рабочий минимум для фотографий: 320, 640, 960, 1280 и 1920 пикселей по ширине. Для карточек товара в сетке хватает двух размеров, 300 и 600.

Исходник храните в максимальном качестве, производные генерируйте один раз и раскладывайте рядом либо отдавайте через imgproxy с дисковым кэшем. Если превью собираются на этапе сборки статики, момент и место генерации зависят от выбранной модели рендеринга: разбор SSR, SSG и гибридных схем есть в материале про стратегии рендеринга и пайплайн данных. Ленивую загрузку картинок ниже первого экрана проще всего включить атрибутом loading="lazy", а для сценариев с подгрузкой по скроллу пригодится связка с Intersection Observer из статьи про динамическую загрузку контента.

Автоматический ресайз и кроп

# imgproxy: вписать в 600x400 без обрезки
/rs:fit:600:400/plain/s3://bucket/photo.jpg@webp

# imgproxy: заполнить 600x400 с обрезкой и умной гравитацией
/rs:fill:600:400/gravity:sm/plain/s3://bucket/photo.jpg@avif

# Thumbor: умный кроп 300x300
/unsafe/300x300/smart/images/photo.jpg

# Thumbor: ограничение по высоте и формат
/unsafe/0x400/filters:format(webp)/images/photo.jpg

Режим fill обрезает изображение до точных пропорций, fit вписывает целиком и оставляет поля. Умная гравитация (gravity:sm в imgproxy, режим smart у Thumbor) ищет лица и контрастные области. Работает это не всегда: на фото товара с мелкой деталью алгоритм срежет именно её, а на схеме выберет неверный фокус. Для критичных карточек задавайте точку фокуса вручную или храните координаты в базе.

Адаптивную выдачу делает разметка, а не сервер:

<picture>
  <source type="image/avif" srcset="/img/photo-600.avif 600w, /img/photo-1200.avif 1200w"
          sizes="(max-width: 600px) 100vw, 600px">
  <source type="image/webp" srcset="/img/photo-600.webp 600w, /img/photo-1200.webp 1200w"
          sizes="(max-width: 600px) 100vw, 600px">
  <img src="/img/photo-600.jpg" srcset="/img/photo-600.jpg 600w, /img/photo-1200.jpg 1200w"
       sizes="(max-width: 600px) 100vw, 600px" width="600" height="400"
       alt="Товарное фото" loading="lazy" decoding="async">
</picture>

Атрибуты width и height заранее резервируют место под картинку и убирают сдвиг вёрстки, из-за которого растёт CLS. Без sizes браузер скачает самый широкий вариант или ошибётся с выбором, а srcset без дескрипторов ширины работает только с плотностью пикселей.

Типичные ошибки и как их избежать

  1. Двойное сжатие. Повторное кодирование WebP в JPEG или новый WebP из прежнего копит артефакты. Стройте производные от мастер-файла, а не от другой производной.
  2. Потеря EXIF и ориентации. Утилиты со --strip-all удаляют тег Orientation, и вертикальные снимки с телефона ложатся на бок. Перед сжатием применяйте авто-ориентацию (imgproxy делает это по умолчанию) или копируйте метаданные: jpegtran -copy all. Если EXIF нужен для аналитики, вырезайте только GPS, а не весь блок.
  3. Некорректный Content-Type. Файл .webp, отданный как application/octet-stream, часть браузеров скачает вместо показа. Проверьте mime.types и ответ командой curl -I.
  4. Отсутствие кэша. imgproxy и Thumbor без кэша и лимита воркеров кладут CPU на потоке однотипных запросов. Настройте кэш, TTL и ограничение параллельных задач.
  5. Прозрачность при конвертации в JPEG. Альфа-канал превращается в чёрный или белый фон. Накладывайте изображение на нужный цвет перед переводом в JPEG или отдавайте такие файлы в PNG и WebP.
  6. Игнорирование Accept и отсутствие Vary. Отдача AVIF всем подряд или кэширование без Vary: Accept ломает картинки у части клиентов. Решение: выбор формата по Accept, заголовок Vary: Accept и JPEG в fallback.

Сравнение инструментов: что выбрать для вашего стека

КритерийNginximgproxyThumbor
Форматы на выходеJPEG, PNG, GIF, WebP и AVIF только через сторонние модулиJPEG, PNG, WebP, AVIF, GIFJPEG, PNG, WebP, AVIF в версии 7
Обработка на летуресайз, кроп, поворотполная, включая умный кропполная, богатый набор фильтров
Зависимостинетодин статический бинарник или DockerPython, Pillow или libvips, OpenCV
Нагрузка на CPUминимальная, работает со статикойсредняя, libvips экономиченвыше, Python-воркеры
Масштабированиевместе с nginxstateless, легконужен общий кэш результатов
Подпись URLнетIMGPROXY_KEY и IMGPROXY_SALTSECURITY_KEY
Сложность запусканизкаясредняявысокая

Рекомендации по стеку. Если nginx уже раздаёт статику и нужен только WebP, хватит предварительной генерации вариантов и map по Accept. Если нужны AVIF, кроп и ресайз по требованию, ставьте imgproxy: он дешевле по ресурсам и проще масштабируется горизонтально. Если требуются фильтры, водяные знаки и умный кроп на OpenCV, а Python в инфраструктуре привычен, берите Thumbor.

Интеграция почти всегда одинаковая: сервис обработки живёт за reverse proxy, исходники лежат в S3-совместимом хранилище, готовые ответы кэширует CDN. В Docker Compose это два контейнера, в Kubernetes - Deployment плюс Service и HorizontalPodAutoscaler по CPU. Учитывайте два момента: кэш должен быть общим для всех реплик, а ключ подписи хранится в секретах, а не открытой переменной в манифесте.

Заключение

Соберите схему в таком порядке:

  1. Мастер-файлы в PNG или JPEG q95+ с сохранённой ориентацией, отдельно от производных.
  2. Форматы выдачи: AVIF первым, WebP вторым, JPEG как fallback для старых клиентов.
  3. Качество: JPEG 80-85, WebP 75-80, AVIF 60-70, проверка по SSIM и визуальному сравнению на 10-15 типах изображений.
  4. Раздача: nginx с map по Accept и заголовком Vary: Accept либо imgproxy с IMGPROXY_ENABLE_AVIF_DETECTION, подписью URL и общим кэшем.
  5. Превью: 320, 640, 960, 1280 и 1920 пикселей, srcset и sizes в разметке, width и height для стабильной вёрстки.

Проверьте результат в трёх браузерах (Chromium, Firefox, Safari), на мобильном экране и командой curl -I для контроля Content-Type, Vary и Cache-Control. Одна такая проверка до релиза дешевле, чем разбор жалоб на чёрные квадраты вместо фотографий.

Поделиться:
Сохранить гайд? В закладки браузера