Оптимизация графики для небольших сайтов: quetzli, webp, avif

в 13:54, , рубрики: avifenc, Guetzli, mezzanine, python, WebP, Разработка веб-сайтов

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

Правый контейнер ощутимо "царапает" глаз
Правый контейнер ощутимо "царапает" глаз

Текст может быть интересен тем, кто живёт примерно также, как мы.

А как это?
  1. Сайт когда-то был на готовом open source движке (мы использовали Mezzanine), но постепенно мутировал так, что можно считать его самописным.

  2. Изображения на сайт выкладывают только сотрудники. Нет никакого user generated content. Поэтому изображений не очень много, место на диске экономить незачем, на одно добавление картинки приходятся десятки тысяч просмотров.

  3. Python. У нас до сих пор 2.7 во многих местах, поэтому приходится немного патчить.

1. Отдельная версия изображений для экранов с retina.

При загрузке первой страницы проверяем window.devicePixelRatio и по нему решаем, показывать «обычные» изображения, или изображения двойного разрешения. Заодно выставляем cookie чтобы повторные визиты на сайт сразу давали нужные настройки. Для изображений обычного размера используется сжатие jpeg с качеством 90, а для изображений двойного размера – с качеством 75 или 80. Это даёт примерно 50-100% увеличение размера файла под "ретину". То есть, изображение вчтеверо большей площади по размеру не более чем вдвое больше исходного. Достоинства:

  • Работает даже на самых древних браузерах.

  • Управляемо: можно для отладки задать в URL параметр, который «насильно» включает или выключает retina и увидеть соответствующие изображения.

Недостатки:

  • Непонятно что делать с самым первым просмотром первой страницы: или показывать обычные изображения, а «красивые» – только при просмотре следующей страницы, когда мы уже узнали значение window.devicePixelRatio. Или, наоборот, сразу показывать «красивые» и начинать экономить только со второй страницы. Или узнавать наличие retina по косвенным признакам (user agent). Мы тогда выбрали первый из этих трёх вариантов.

  • Про то, что может быть ноутбук с retina и пристёгнутый к этому же ноутбуку второй экран без retina мы тогда даже не думали.

  • Сложно кэшировать: каждую страницу сайта нужно сохранять в варианте с retina и в варианте без.

Увидели ли мы рост посещаемости сайта или повышение конверсии от внедрения? Нет.

2. Минимизируем трафик при «кажущемся» высоком качестве изображений.

В 2016 году появилась библиотека Guetzli. Авторы сделали очень вычислительно тяжёлый алгоритм, который подбирает параметры сжатия изображения так, чтобы воспринимаемые глазом потери были минимальны. Для Python есть модуль pyguetzli, который довольно просто подключить.

Guetzli хорошо сжимает картинки, но работает адски долго. В нашей CMS масштабирование графики делается прямо во время формирования html-кода страницы. Разумеется, только если в файловой системе ещё нет нужного файла с масштабированной картинкой. Но если, вдруг, его нет (новая страница, заменили изображение, сбросили кэш изображений на сервере), то пока CMS не отмасштабирует все изображения со страницы под заданные размеры, html-код не будет сформирован, посетитель будет ждать.

Со включённым алгоритмом guetzli этот процесс может занимать несколько минут. Это создаёт риск «не дождался загрузки страницы, нажал refresh, снова не дождался, нажал ещё раз refresh, сайт надорвался». Поэтому мы сделали двухпроходную схему:

  1. Сначала мы масштабируем изображение обычным кодом из библиотеки Pillow.

  2. Около файла myimage-200x200.jpg помещаем «теневой файл» myimage-200x200.jpg-guetzli-me, по которому мы знаем, что это изображение сжато быстрым, но неоптимальным алгоритмом.

  3. Отдельным проходом запускаем процесс, который ищет все изображения с суффиксом -guetzli-me в имени файла и формирует их повторно, с использованием guetzli. Важно: формировать изображения надо из исходной картинки, а не из масштабированного на п. 1 файла. Иначе вся затея с качественными картинками теряет смысл.

Что мы в итоге получили:

  • Минимизировали вес страниц на сайте, размер изображений свели к минимуму. Экономия составила примерно 25% от общего размера загружаемых данных.

  • Не боимся сброса кэша или коллеги, которая решила заменить все изображения на странице на новые. Эта страница какое-то недолгое время будет иметь неоптимизированные по размеру файлов изображения, ничего страшного.

  • Совместимо со всеми браузерами.

Увидели ли мы рост посещаемости сайта или повышение конверсии от уменьшения веса страниц? Нет.

3. Уходим от javascript с window.devicePixelRatio

В какой-то момент проверка разрешения на стороне клиента начала сильно мешать. Server Side Rendering должен быть согласован с клиентским кодом, работающим в браузере. Проблема первой загрузки, когда мы ещё не знаем devicePixelRatio, никуда не делась. Мы модифицировали CMS, чтобы во все <img src="..."> автоматически подставлялся атрибут srcset="...", в котором мы задаём разные URL изображений для разных разрешений экрана. Примерно так:

<img src="/m/.../200x200-10.jpg"
        srcset="/m/.../200x200-10.jpg 1x, /m/…/200x200-20.jpg 2x"
        width="200" height="200">

Этот вариант не работает на старых браузерах iPhone с retina (телефоны с ретиной выпускались начиная с 2010 года, а атрибут srcset поддерживать начали только в 2014), но этим уже тогда можно было пренебречь: количество таких телефонов было примерно равно нулю. В результате и первая и все последующие страницы загружаются с правильными изображениями. Про компьютеры, к которым подключено несколько разных дисплеев, также можно не думать.

4. Изображения с прозрачным фоном.

Для изображений с прозрачным фоном использовать jpeg не получается. Приходится их выкладывать в формате PNG. По состоянию на январь 2023 года перейти на WEBP в качестве основного формата изображений всё ещё нельзя: есть пользователи с Safari 13 и другими браузерами, которые этот формат не поддерживают.

Для оптимизации PNG по размеру можно использовать Pngquant. Код может выглядеть примерно так:

if strFormat == 'PNG':
    oImage.convert("RGBA").quantize(method=3).save(strFileName, strFormat, ...)

5. Фотографии с прозрачным фоном, webp

В какой-то момент на сайте появились фотографии, на которых поля должны быть прозрачными. Держать их в PNG – расточительно, они занимают неоправданно много места. Подключим WEBP.

Этот формат поддерживается библиотекой Pillow "из коробки". Поэтому достаточно установить библиотеки для webp, перекомпилировать Pillow, убедиться, что он их почуял, и вызвать:

oImage.save(strWebpShadowFilename, "WEBP", ...)

При тестировании оказалось, что сформированные кодом на Питоне файлы WEBP были всегда больше по размеру, чем сформированные утилитой командной строки cwebp. Оказалось, что библиотека Pillow версии 6.2.2 (а это последняя версия, которая работает с Python 2.7, все более свежие работают только с третьим Питоном) просто игнорирует параметр method, задающий баланс время-работы / размер-выходного-файла. Мы вызывали в python

oImage.save(strWebpShadowFilename, "WEBP", method=6, ...)

а оно этот параметр просто не дотаскивало до кода на C, который, собственно, и реализует сохранение в формате WEBP. Раз уж полезли в это место, сделали патч: версия 6.2.2 библиотеки Pillow, которая принудительно передаёт метод компрессии, равный 6. Да, мы никуда не торопимся, у нас немного изображений и они не так уж часто добавляются. Если кто-то ещё не ушёл с Python 2.7, то вот патч, а вот модифицированная версия 6.2.2 библиотеки Pillow, можно установить её при помощи pip и пользоваться.

6. Avif.

Раз уж влезли в это место, решили сразу встроить и поддержку ещё более свежего формата изображений, AVIF. Он пока не поддерживается библиотекой Pillow, нужно устанавливать отдельную библиотеку pillow_avif. И снова оказалось, что изображения, сформированные кодом на Питоне, имеют больший размер, чем если их формировать утилитой командной строки avifenc. Оказалось, что в обёртка на python делает вид, что понимает параметр quality, и интерпретирует его также, как и во всех остальных форматах (100 - самое лучшее качество, 80 - это "для сайта" и т. д.). Но реализовано это было очень странно (см. исходник):

    qmin = info.get("qmin")
    qmax = info.get("qmax")
    if qmin is None and qmax is None:
        # ...
        quality = info.get("quality", 75)
        # ...
        qmin = max(0, min(64 - quality, 63))
        qmax = max(0, min(100 - quality, 63))

Параметры сжатия qmin и qmax вычислялись из параметра quality таким образом, что итоговая картинка AVIF была зачастую больше по размеру, чем JPG. Вылечили чтением исходного кода утилиты командной строки avifenc и взятием параметров qmin/qmax оттуда.

Сжатие AVIF работает медленно. Быстрее, чем quetzli, но всё равно недостаточно быстро, чтобы это делать «на лету», при формировании html-кода страницы. Поэтому мы его вынесли в "медленный" процесс, сразу после сжатия jpeg алгоритмом guetzli.

7. Как это всё использовать на сайте?

Проще всего встроить поддержку webp и avif методом тюнинга сервера nginX, примерно так:

    set $webp_suffix "";
    if ($http_accept ~* "webp") {
            set $webp_suffix ".webp";
    }
    location ~* .(jpeg|jpg|png)$ {
            add_header Vary "Accept-Encoding";
            try_files $uri$webp_suffix $uri $uri/ =404;
            add_header Cache-Control public;
    }

Аналогично можно сделать и для формата AVIF. Но мы так делать не стали. Причины:

  • Разные браузеры присылают заголовок Accept по разному. Например, Safari for iOS версии 14 не присылает строку webp при обращении к странице, но присылает при загрузке изображения с этой страницы. Здесь можно посмотреть, насколько разнообразен этот зоопарк.

  • Это трудно тестировать и отлаживать: подменить заголовок Accept сложно. Непросто даже в логах на сервере увидеть, какое именно изображение было выдано конкретному браузеру.

Поэтому встроили это "классическим" способом:

<picture>
  <source type="image/avif"
      srcset="/m/.../200x200-10.avif 1x, /m/…/200x200-20.avif 2x">
  <source type="image/webp"
      srcset="/m/.../200x200-10.webp 1x, /m/…/200x200-20.webp 2x">
  <img src="/m/.../200x200-10.jpg"
      srcset="/m/.../200x200-10.jpg 1x, /m/…/200x200-20.jpg 2x"
      width="200" height="200">
</picture>

Разумеется, в нескольких местах поломалась вёрстка из-за правил:

.some-class > img {
    ...
}

На некоторых изображениях получается, что файлы webp или avif по размеру больше, чем файлы jpg. Для них совершенно корректно работает вот такая оптимизация:

<picture>
  <source type="image/avif"
      srcset="/m/.../200x200-10.jpg 1x, /m/…/200x200-20.avif 2x">
      <!--                     ^^^^                           -->
  <source type="image/webp" 
      srcset="/m/.../200x200-10.webp 1x, /m/…/200x200-20.jpg 2x">
      <!--                                              ^^^^  -->
  <img src="/m/.../200x200-10.jpg" 
      srcset="/m/.../200x200-10.jpg 1x, /m/…/200x200-20.jpg 2x"
                width="200" height="200">
</picture>

Все браузеры нормально обрабатывают ситуацию, когда им обещали дать изображение формата WEBP, а реально дали обычный JPEG.

Итоги:

  • Внедрение webp дало дополнительные 15% уменьшения размера изображений. Avif даёт больше, примерно 30%. При этом формируемый html-код и таблицы стилей немного потяжелели, примерно на 5%.

  • На многих файлах webp и avif дают совсем небольшую экономию по сравнению с guetzli, а иногда просто занимают больше места, чем jpeg. Чаще всего это можно наблюдать на на маленьких изображениях (200х200 px).

  • Если не внедрять сжатие guetzli, а сразу встроить webp и avif, то экономия более заметна. Но полностью уйти от jpeg всё равно пока не получается: браузеры, поисковые машины, превью-боты работают именно с этим форматом.

  • Необходимости внедрять webp и avif для 100% страниц сайта при наличии guetzli нет. Если одно-два изображения отдаются "по старинке", без тега picture, это настолько мало сказывается на общем времени загрузки, что можно задушить внутреннего перфекциониста и отложить 100% внедрение на потом.

Увидели ли мы ускорение работы сайта при переходе на webp/avif на Яндекс.Метрике? Да, метрика "время до загрузки DOM" уменьшилась примерно на 15%.

Увидели ли мы рост посещаемости сайта или повышение конверсии от уменьшения веса страниц? Нет.

О компании GrinDin: мы с 2011 года занимаемся доставкой здорового питания по Москве и Московской области.

Автор: Владислав Шабанов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js