Оптимизация изображений

Узнайте, как сократить размер изображений без ухудшения качества и оптимизировать их для веб-страниц с помощью конвертации в формат WebP.

· 6 минуты на чтение

Размер изображений составляет значительную часть от размера всей страницы. Поэтому важно сжимать изображения и использовать правильный формат.

Спонсор поста

Сжатие PNG/JPG

Я подготовил специальный sh-скрипт, который автоматизирует процесс сжатия.

#!/bin/bash
file=comp.flag

if [ -f "$file" ]; then
    option="-newer $file"
fi

find ./images/ -type f -not -path "./images/comp/*" ! -name "*-no-comp.*" $option -iname "*.png" -exec sh -c '
  png_file="${1/\/images\//\/images\/comp\/}"
  png_dir="$(dirname "$png_file")"
  mkdir -p "$png_dir"
  cp "$1" "${png_file}"
  optipng -o7 "${png_file}"
  advpng -z4 "${png_file}"
  pngcrush -rem gAMA -rem alla -rem cHRM -rem iCCP -rem sRGB -rem time -ow "${png_file}"
' _ {} \;

find ./images/ -type f-not -path "./images/comp/*" ! -name "*-no-comp.*" $option -iregex '.*\.\(jpg\|jpeg\)' -exec sh -c '
  jpg_file="${1/\/images\//\/images\/comp\/}"
  jpg_dir="$(dirname "$jpg_file")"
  mkdir -p "$jpg_dir"
  cp "$1" "${jpg_file}"
  jpegoptim --all-progressive "${jpg_file}"
' _ {} \;

touch $file
echo "$(date)" > $file

Этот скрипт сжимает изображения без потери качества. Он размещается рядом с каталогом images, в котором находятся ваши изображения. Разберемся, как он работает.

Сначала скрипт проверяет, существует ли в текущем каталоге файл comp.flag. Если файл существует, он устанавливает значение -newer $file в переменную option, которая будет использоваться в качестве фильтра для поиска только тех файлов, которые были изменены после даты создания файла optimg.flag. Если файл не существует, переменная option будет пустой.

Затем скрипт использует команду find для рекурсивного поиска файлов в каталоге images и его подкаталогах, которые:

  • -type f. Являются обычными файлами.
  • Имеют расширение имени файла .png, .jpeg или .jpg.
  • -not -path. Не находятся в указанных каталогах. В данном случае это каталоги ./images/comp. Мы будем складывать туда сжатые изображения, и мы не хотим заново по ним проходить поиском и сжимать снова.
  • ! -name "-no-comp.". Оставим возможность не сжимать изображение, если его имя заканчивается на -no-comp.
  • Для каждого из этих файлов сценарий использует свою команду оптимизации.

Мы не будем затирать оригиналы изображений. Вместо этого мы создадим дополнительную папку comp в каталоге images, в которую и сложим преобразованные изображения. Создаваемая структура подкаталогов в comp будет повторять структуру подкатологов в images.

Для файлов PNG сначала используется optipng для сжатия с самым высоким уровнем оптимизации (-o7). Далее используем advpng для дальнейшего сжатия с уровнем сжатия 4 (-z4). И наконец pngcrush для удаления из файла определенных фрагментов, которые можно безопасно удалить для уменьшения размера файла.

Для файлов JPEG используется jpegoptim для их оптимизации со сжатием без потерь (--all-progressive --strip-all).

После оптимизации всех подходящих изображений сценарий пересоздает файл comp.flag. Это гарантирует, что скрипт будет оптимизировать только те файлы, которые были изменены с момента последнего запуска.

Тесты на сжатия

Возьмем два одинаковых изображения (3456  x  2234): одно форматом jpg и размером 2.2 мб, второе форматом png и размером 2,7 мб. Прикрепляю архив с тестовыми данными, чтобы вы могли повторить тесты у себя.

Запускаем скрипт и смотрим на результат сжатия.

Выполнение optipng занимает очень много времени, наберитесь терпения.

Вывод в консоль

** Processing: ./images/comp/image-two.png
3456x2234 pixels, 4x8 bits/pixel, RGB+alpha
Input IDAT size = 2664231 bytes
Input file size = 2666952 bytes

Trying:
  zc = 9  zm = 9  zs = 0  f = 0         IDAT size = 2374591
  zc = 9  zm = 8  zs = 0  f = 0         IDAT size = 2368803
  zc = 9  zm = 9  zs = 0  f = 1         IDAT size = 2142264
  zc = 9  zm = 8  zs = 0  f = 1         IDAT size = 2137390
  zc = 9  zm = 9  zs = 1  f = 1         IDAT size = 2134232
  zc = 9  zm = 8  zs = 1  f = 1         IDAT size = 2128765
  zc = 9  zm = 9  zs = 0  f = 4         IDAT size = 2074935
  zc = 9  zm = 8  zs = 0  f = 4         IDAT size = 2071117
  zc = 9  zm = 9  zs = 1  f = 4         IDAT size = 2055799
  zc = 9  zm = 8  zs = 1  f = 4         IDAT size = 2054390
  zc = 9  zm = 9  zs = 0  f = 5         IDAT size = 2046026
  zc = 9  zm = 8  zs = 0  f = 5         IDAT size = 2040193
  zc = 9  zm = 9  zs = 1  f = 5         IDAT size = 2031130
  zc = 9  zm = 8  zs = 1  f = 5         IDAT size = 2024568
                               
Selecting parameters:
  zc = 9  zm = 8  zs = 1  f = 5         IDAT size = 2024568

Output IDAT size = 2024568 bytes (639663 bytes decrease)
Output file size = 2025345 bytes (641607 bytes = 24.06% decrease)

     2025345     2025345 100% ./images/comp/image-two.png (Bigger 2208631)
     2025345     2025345 100%
Warning: versions are different between png.h and png.c
  png.h version: 1.6.34
  png.c version: 1.6.37

  Recompressing IDAT chunks in ./images/comp/image-two.png
   Total length of data found in critical chunks            =   2024625
   Best pngcrush method        =  10 (ws 15 fm 6 zl 9 zs 1) =   2055326
CPU time decode 1.074758, encode 16.759768, other 0.018592, total 17.899076 sec
./images/comp/image-one.jpg 3456x2234 24bit N Exif XMP JFIF  [OK] 2158567 --> 1947377 bytes (9.78%), optimized.

В результате мы смогли сжать png до 2.1 мб, а jpg до 1.9 мб. При этом сохранив исходный размер изображения и без потерь качества.

Лучшее, на мой взгляд, приложение для сжатия jpg это JPEGmini Pro. Имеет версию cli для серверов. Но к сожалению оно платное. Его результат сжатия 2.2 мб —> 959 кб.
Рандомный блок

Преобразование в WebP

PNG и JPG являются хорошими форматами изображений, которые можно сжать без потери качества. Однако, на сегодняшний день существует более современный формат WebP, который может показать еще более эффективные результаты при сжатии изображений, но с едва заметным ухудшением качества.

В документации WebP перечислено множество параметров, которые повлияют на качество получаемого изображения. Вы можете провести экспериментальный подбор параметров онлайн на сайте https://squoosh.app.

Я вы выбрал для себя следующий набор параметров:

cwebp -mt -af -progress -m 6 -q 80 -pass 10 input.jpg -o output.webp

Это команда преобразует JPG файл input.jpg в файл изображения WebP с именем output.webp. Команда включает несколько опций, которые управляют процессом кодирования:

  • -mt включает многопоточность, что может улучшить производительность  на многоядерных процессорах.
  • -af включает автоматическую фильтрацию, которая применяет алгоритм фильтрации для повышения эффективности сжатия.
  • -progress выводит показывает процент обработки файла.
  • -m 6 устанавливает максимальное количество сегментов, используемых в процессе кодирования, равным 6, что может повысить эффективность сжатия за счет увеличения времени кодирования.
  • -q 80 устанавливает коэффициент качества на 80, который контролирует степень сжатия и влияет на визуальное качество выходного изображения.
  • -pass 10 устанавливает число проходов кодирования равным 10, что может повысить эффективность сжатия за счет увеличения времени кодирования.
-lossless позволяет использовать сжатие без потерь. Но тогда ваше новое изображение может оказаться существенно тяжелее исходного.

Добавим в наш скрипт преобразование в WebP.

find ./images/comp -type f -iregex '.*\.\(jpg\|jpeg\|png\)' -not -iregex '.*no-comp\.\(jpg\|jpeg\|png\)' $option -exec sh -c '
  webp_file="${1/\/images\/comp\//\/images\/webp\/}"
  webp_dir="$(dirname "$webp_file")"
  mkdir -p "$webp_dir"
  cwebp -mt -af -progress -m 6 -q 75 -pass 10 "$1" -o "${webp_file%.*}.webp"
' _ {} \;

Мы берем сжатые изображения из папки comp и преобразуем их в WebP, складывая в отдельную папку webp. Если вы захотите использовать другие параметры сжатия, вы всегда сможете пересоздать изображения с новыми параметрами.

Тесты преобразования

Продолжим наши эксперементы со сжатием. Теперь сожмем наши файл размером в 2,7 мб и 2.2 в формат WebP с разными параметрами качества:

  • -q 90: Размер 325 кб.
  • -q 85: Размер 267 кб.
  • -q 80: Размер 229 кб.
  • -q 75: Размер 200 кб.
  • -q 70: Размер 191 кб.
  • -q 60: Размер 176 кб.
  • -q 50: Размер 163 кб.
  • -q 40: Размер 147 кб.
  • -q 30: Размер 129 кб.
  • -q 20: Размер 115 кб.
  • -q 10: Размер 90 кб.
  • -q 1: Размер 70 кб. Для экстренных случаев. Например, вы заблудились в лесу и надо отправить фото по спутниковой сети 😅

Nginx

Теперь мы научим nginx при запросе изображений сначала пытаться найти WebP файл, и только потом отдавать сжатый PNG/JPG, а если и сжатого нет, то отдавать обычный файл.

Для этого напишем следующий location:

location ~* ^(/images/)(.+)\.(png|jpe?g)$ {	
    add_header Cache-Control 'public,max-age=31536000';
    alias /images;
    set $webp_image_subdir "/webp/";
    set $comp_image_subdir "/comp/";
    set $basename $2;
    try_files $webp_image_subdir$basename$webp_suffix $comp_image_subdir$basename.$3 $uri;
}

Данный location обрабатывает все запросы к адресам, которые начинаются с /images/, за которым следует любое количество символов, затем точка, а затем формат файла png/jpeg/jpg.

Для запросов, соответствующих этому шаблону, выполняются следующие действия:

  • Директива alias указывает путь к локальной директории, из которой будут отдаваться файлы. В данном случае путь к директории /images.
  • Директивы set назначают переменные, которые будут использоваться в последующих директивах. Переменная $webp_image_subdir устанавливается в /webp/, а переменная $basename устанавливается в захваченную подстроку шаблона регулярного выражения (т.е. имя файла без расширения).
  • Директива try_files пытается отдать Webp-версию запрошенного файла изображения, добавляя к переменным $webp_image_subdir и $basename суффикс .webp. Если NGINX сможет найти Webp-версию запрашиваемого файла изображения, он отдаст его. Если он не может найти WebP-версию, то отдаст сжатый JPG/PNG, если и сжатого файла не будет, то отдаст не сжатое.

Заключение

Вот к такому скрипту оптимизации изображений я пришел. Если вы знаете как его улучшить, то обязательно оставляйте свои комментарии.

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇