Отдаем модные форматы картинок в webp и avif напрямую через nginx и apache в обход разметки



Всем привет!
Я тут работал над одним проектом, в котором очень много контентной и интерфейсной графики, десятки тысяч изображений и, конечно, возник вопрос оптимизации сайта, чтобы удовлетворить требования поисковых систем.
Энтузиазма добавили, появившиеся относительно недавно у гугла, так называемые Core Web Vitals.
Кто не в курсе это пачка технических показателей качества сайта, которые скоро будут включены в алгоритм ранжирования и все тормознутые сайты из-за них, типа, покатятся вниз.
Ну в общем, встала задача оптимизировать картинки, а также сделать так, чтобы не пришлось переписывать кучу html кода, чтобы эти картинки туда вставить.

Как это сегодня делается в HTML:


Тут ничего сверхъестественного, просто кладем пачку картинок по нужному адресу, прописываем все варианты изображений в разных форматах и браузер сам выберет, что ему по силам.
Отличный вариант, с прогрессивным подходом — но как только дело доходит до управления такими картинками из админки, начинаются танцы с бубном, большинство дополнений как и штатные тв-поля с типом изображение дают выбирать одно изображение. И получается, чтобы указать все 3 варианта, нужно либо делать 3 тв-шки, либо мутить в разметке что-то подобное с одной тв-шкой



Я тут показал очень простой вариант, предполагающий, что картинка будет точно в .jpg, а ведь могут загрузить и png и jpeg и.т.д И код выше превращается в мешанину регулярок, а когда картинок много — получается кромешный ад.
Потом приходит в голову вынести эту логику в сниппет, чтобы он занимался всем этим, и потом использовать его как модификатор.

Но на этом моменте я уже не выдержал)

Итак решаем задачу «по умному», но сразу скажу, что этот вариант подойдет только для тех, кто имеет доступ к конфигурации web-сервера nginx, то есть большинство шаред-хостингов тут сразу пролетают, так как у них вечно все гайки закручены и раскрутке не подлежат, за исключением modhost.pro и хостингов, где за отдачу статики отвечает apache.

Итак шаг первый:

Сначала надо убедиться, что наш веб-сервер способен понимать файлы с расширением .avif и .webp
Обычно в основном конфиге nginx есть директива include которая подключает файлик содержащий все типы файлов с которыми может работать веб-сервер. Как правило он называется mime.types
Проверяем в нем наличие наших форматов картинок, если их нет добавляем


Шаг второй

Теперь надо добавить проверку заголовка поддержки форматов от браузера, он называется accept и выглядит вот так


Список поддерживаемых форматов будет зависеть от браузера, в данном случае это хром
Проверять этот заголовок мы будем также в основном конфиге в контексте http {}
Добавляем туда


Этот кусок конфига создаст 2 переменные $webp_suffix и $avif_suffix, они будут содержать расширение файла, если браузер его поддерживает или пустую строку если нет.

Шаг третий

Теперь нужно задействовать наши переменные и делать мы это будем уже в конфиге конкретного хоста в контексте server{}


Добавляем нужный локейшн с регуляркой, где фильтруем только картинки в формате jpg и png и дальше начинаем «клеить строки».
set $base $1;
Сначала создается переменная $base которая будет содержать путь до файла без расширения
Пример: /assets/images/test

Далее создаем ещё одну переменную $webp_uri которая формируется из склейки предыдущей переменной + переменной, которую мы делали на прошлом шаге — $webp_suffix
set $webp_uri $base$webp_suffix;
Это даст нам строку: /assets/images/test.webp

Далее ещё одну переменную по такому же принципу, но приклеиваем другую переменную — $avif_suffix
set $avif_uri $base$avif_suffix;
Это даст нам строку: /assets/images/test.avif

add_header X-uri "$base $webp_uri $avif_uri";
Дальше идет тестовый заголовок, я его добавил вместо того, чтобы выводить инфу о переменных в лог файл, удобно для быстрого тестирования. Этот заголовок выведет всё, что содержится в предыдущих переменных. Его потом нужно будет удалить, когда всё проверите.

add_header Vary Accept;
Следом ещё один заголовок — он для кэширования на прокси-серверах, его здесь не буду разбирать.

Ну а дальше самая главная строка:

try_files $avif_uri $webp_uri $uri @rewrite;
Директива try_files проверяет доступность адресов по порядку и мы ей говорим следующее:
Попробуй найти файл с расширением avif, если такого нет попробуй найти webp, если и такого нет, попробуй обычный формат jpg, png если и их нет отдается обработка в php, но можно и вернуть 404 ошибку, тут кто как настраивает логику работы.

После всего этого, нужно перезапустить nginx, чтобы новые конфиги вступили в силу
И переходим к тестам:
Я сделал фотки во всех нужных форматах



Проверяем, что там нам отдаст браузер


Обратите внимание — слева видно, что запрашивался файл png, но сервер нашел вариант получше и вернул avif

Если я удаляю физически картинку в формате avif, получаем следующее


Теперь получили webp, если и его удалить, то получим изначальную png-шку

Ну и самое главное, теперь в разметке достаточно старого доброго img



Менеджеры в админке выбирают одну картинку, а сервер решает какую сможет показать браузер посетителя.
Вот, собственно, и всё!

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



Просто ставим галку отключения avif — и браузер вдруг разучивается понимать avif картинки, тоже самое с webp
Так тестировать куда приятнее!

P.S:
На одном проекте всё же пришлось решать вопрос со сквозной отдачей картинок через Apache, важно только отметить, что данное решение будет работать если Apache отвечает за отдачу статики, но к сожалению (на самом деле к счастью) на большинстве шаред-хостингов используется комбинация из Apache+Nginx, и за статику отвечает Nginx, так что снова приплыли к основному содержанию статьи) Ну или использовать хостинг — modhost.pro, где можно править конфиг Nginx-а

Короче решение для чистого Apache:
В файлике .htaccess добавляем следующее
<IfModule mod_rewrite.c>
  RewriteEngine On
  
  # Если нужно ограничить какой-то директорией, раскомментировать следующую строку
  #RewriteCond %{REQUEST_URI} ^/assets/images/
  RewriteCond %{HTTP_ACCEPT} image/avif
  RewriteCond %{DOCUMENT_ROOT}/$1.avif -f
  RewriteRule (.+)\.(jpe?g|png)$ $1.avif [T=image/avif,L,NC]
  
  # Если нужно ограничить какой-то директорией, раскомментировать следующую строку
  #RewriteCond %{REQUEST_URI} ^/assets/images/
  RewriteCond %{HTTP_ACCEPT} image/webp
  RewriteCond %{DOCUMENT_ROOT}/$1.webp -f
  RewriteRule (.+)\.(jpe?g|png)$ $1.webp [T=image/webp,L,NC]
</IfModule>

<IfModule mod_mime.c>
  AddType image/webp .webp
  AddType image/avif .avif
</IfModule>

Принцип работы абсолютно такой же, если рядом с оригинальной картинкой будет лежать аналог в webp или avif — веб сервер отдаст именно их, если нет, то оригинальное изображение.
Семён Кудрявцев
26 апреля 2021, 23:44
modx.pro
20
5 423
+21

Комментарии: 12

Антон Тарасов
27 апреля 2021, 08:12
+1
Спасибо! Очень своевременная и важная инструкция.
    Николай Савин
    27 апреля 2021, 08:57
    0
    Познавательно!
      Игорь
      27 апреля 2021, 21:54
      0
      Спасибо! Как я понял, здесь указано как работать с {$image}. Не сочтите за наглость, а как правильно прописать конфигурацию конкретного хоста, при использовании {$thumb}? Возможна ли генерация изображений в формате avif?
        Семён Кудрявцев
        27 апреля 2021, 22:20
        +2
        В статье речь ни о image и ни о thumb, шаблонизатор и доступные плейсхолдеры тут ни при чем. Речь про обработку запросов изображений веб-сервером. Генерировать avif пока нельзя. Насколько знаю phpThumb его пока не поддерживает. Но самому сделать легко, куча сервисов и программ есть. Я их генерирую через nodejs и кладу рядом с оригиналом. То есть MODX не занимается у меня генерацией webp и avif от слова совсем. Все делает нода, формирует эти файлы всегда рядом с тем, что назагружали контент-менеджеры.
          Николай Савин
          28 апреля 2021, 08:41
          0
          Интересно, а скрипт ноды для генерации изображений все время активен на сервере или запускается по расписанию?
          Там какой то потоковый сборщик запущен с нужным пайпом или чисто JS скрипт с библиотекой?
            Семён Кудрявцев
            28 апреля 2021, 09:29
            1
            +4
            Да он висит постоянно, как обычно делают во всяких сборках типа gulp, webpack когда работают с картинками вешают вотчеры, которые колдуют над ними.
            Вот такая же фигня запущена на ноде на серваке, важно только там ноду под тем же пользователем запустить, что и весь сайт, а то проблемы с правами поползут.
            Я использую для наблюдения — github.com/paulmillr/chokidar
            А для генерации любых картинок с любыми настройками — github.com/GoogleChromeLabs/squoosh/tree/dev/cli
            Это консольный аналог этого сервиса — squoosh.app/
              Алексей
              05 мая 2021, 17:36
              0
              а есть пример скрипта как пережимаются в другие форматы изображения?
                Семён Кудрявцев
                05 мая 2021, 18:02
                +1
                Скоро опубликую статью про это, там покажу и скрипт и опишу процесс подробнее.
        max
        max
        31 августа 2023, 10:53
        0
        А есть идеи (подсказка), как этот скрипт использовать на modhost.pro? Конструкцию map нужно запихивать в http {}, а на modhost.pro есть доступ только к редактированию директивы server
          Николай Савин
          31 августа 2023, 12:08
          0
          Если не ошибаюсь это невозможно в случае modhost. Уж точно не через map
            max
            max
            31 августа 2023, 12:21
            0
            Вот возможно есть способ написать такой скрипт без использования map
            Семён Кудрявцев
            31 августа 2023, 16:23
            0
            К сожалению директива map работает только в контексте http, поэтому её использовать на modhost.pro не получится.
            Но можно попробовать другой вариант, правда более медленный, но если получится напишите сюда. Я честно его не пробовал, просто первое, что в голову пришло, но теоретически развить этот вариант можно попробовать
            http {
                server {
                    listen 80;
                    
                    location / {
                        if ($http_accept ~* "text/html") {
                            # Do something if the Accept header contains "text/html"
                        }
                    }
                }
            }
            Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
            12