Оптимизация под Pagespeed: работа с изображениями как с наиболее частой и весомой проблемой сайтов

от автора

Сайт успешно протестировали на мастере, выкатили на прод, провели контрольное тестирование — вроде всё хорошо. Он работает пару месяцев — и вдруг приходит задача от SEO «увеличить скорость загрузки» или «исправить просевшее количество баллов в PageSpeed». Причём ничего принципиально нового не добавляли, просто наполняли контентом.

Для примера рассмотрим типичную ситуацию и некогда оптимизированный проект с минимумом изображений на стеке Python/Django/SCSS — на нём будем делать правки.

Для начала смотрим загрузку «на глаз» — вроде нормально. Но отдел SEO упорно ставит задачу и требует исправить баллы в PageSpeed, потому что его метрики влияют на ранжирование в поиске.

Ок, открываем его — и видим оранжевую зону и непонятные аббревиатуры CLS, LCP и др. 

Сайт с рядом правок, которые PageSpeed рекомендует выполнить

Сайт с рядом правок, которые PageSpeed рекомендует выполнить

А всё потому, что PageSpeed ориентирован на наиболее жёсткий вариант условий открытия сайта, и он проверяется без кэша в условиях мобильного интернета (скорость варьируется в рамках от скоростного 3G до медленного 4G).

Начинаем разбираться — и выясняется, что есть общая проблема, которая почти всегда повторяется. А именно — изображения. Их неправильно готовят: не ресайзят, не соблюдают соотношение сторон, не конвертируют, заливают «как есть». Зачастую на мобильной версии сайта вполне можно найти изображение во всю ширину десктопа, сжатое силами CSS. Выглядят одинаково, но вес — совершенно разный. И на вопрос «Почему так сделано?» — ответ, как правило, — «Всё взято с макета, соответствие проверено по perfect pixel» или «с отделом дизайна согласовано, стилистика не нарушается». Получается, что риски надо было закладывать на этапе дизайна и разработки, но по факту их исправляют, когда продакшн уже горит.

Поэтому дальше разберём пользу сжатий и конвертации на живом примере и покажем, как их править. А заодно — как делать, чтобы в процессе разработки они не повторялись.

Пункт 0. Первичная оценка масштабов катастрофы

Открываем DevTools, вкладку «Сеть». Видим запросы. 

Пример с небольшим количеством запросов. В основном — картинки и скрипты

Пример с небольшим количеством запросов. В основном — картинки и скрипты

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

Открываем калькулятор и подсчитываем, какой же объем изображений у нас на сайте грузится в итоге: то, что раньше весило килобайты, теперь перевалило за мегабайты. А ведь это только изображения. Они — весомая, но не единственная часть нагрузки.

Итог: сайт не просто работает, а активно обрастает тяжелым контентом и внешними сервисами. С этим надо что-то делать.

Пункт 1. Смотрим размеры и формат

Дизайнеры нарезают красивые макеты, но в админке картинки заливает контент-менеджер, и его логика проста: «Если поле позволяет загрузить PNG в 4K — почему бы и нет? Предварительная конвертация требует времени, а время — деньги компании». 

А PageSpeed упорно пишет, что размеры изображений выше, чем нужно, и съедают лишние ресурсы. 

Часть изображений не соответствует размерам контейнера на мобильной версии, а также отсутствует сжатие AVIF — выдаёт рекомендации по загрузке изображений и LCP

Что же делать разработчику, чтобы решить проблему? Правильно — заложить обработку на уровне кода заранее. Или, если проект уже в проде, внедрить её сейчас.

Дальнейшие действия выглядят так (Можно выполнять вместе или выборочно. Логично, что изображение AVIF-формата уже оптимальнее, чем JPEG. А если нарезать мобильную версию картинки, она будет легче, чем десктопная).

  • Смотрим, какие размеры изображений используются на ключевых разрешениях и делаем автоматическую нарезку на эти размеры. Например, для фото в шапку, которое идёт на всю ширину страницы, могут быть валидны вариации десктоп (1280px), планшет (768px), мобилка (320px). 

Пример веса изображения, нарезанного для мобильного разрешения

Пример веса изображения, нарезанного для мобильного разрешения
  • Конвертируем в WebP, AVIF или JPG. Часто получается так, что расширение PNG используется для всех изображений подряд, в том числе там, где нет прозрачности. При этом упускается, что даже оптимизированный PNG весит больше JPG. При автоматике наличие прозрачности отследить сложно, но при ручной заливке стоит поменять расширение PNG-исходника на правильное.

    Когда с PNG разобрались, переходим к генерации веб-форматов. Почему именно WebP и AVIF? Потому что именно они проектировались для WEB, и обеспечивают наилучшее соотношение качество / вес / поддержка.

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

Пример разницы в весе изображения в разных форматах

Пример разницы в весе изображения в разных форматах
  • Сжимаем без видимых потерь качества. (Например, как вариант, с помощью Pillow). Для автоматического сжатия можно начать со сжатия на 20% — при таком значении видимые потери в качестве как правило почти незаметны. А дальше можно подогнать по необходимости.

Пример разницы в весе до и после сжатия

Пример разницы в весе до и после сжатия

При конвертации и сжатии стоит учесть:

  • То, что уже залито — зачастую легче перезалить руками. Тогда вы не потеряете контроль над тем, что получилось после нарезки.

    AVIF — это достаточно агрессивное сжатие. При ручной заливке мы вполне можем сделать отображение результата в админке и контролировать качество. При автоматике — можем получить вместо изображения набор цветных полосок. С WebP, как правило, таких проблем нет, но контроль лишним не бывает.

  • Для CKEditor и подобных плагинов — парсим содержимое (BeautifulSoup), подменяем фото, сохраняем в отдельное поле для отображения.

  • Меняем тег <img> на <picture> с источниками для разных форматов и разрешений и вариаций. В этом случае браузер загрузит только ту вариацию, чей атрибут type соответствует его возможностям. Тут стоит учесть: согласно исследованиям, больший объем интернет-трафика используется смартфонами, и Google при индексации сайта использует мобильную версию сайта. Поэтому в теге img указываем в качестве fallback путь к мобильной версии изображения, а в атрибутах width и height — размеры мобильной версии.

Итог этого пункта: даже учитывая, что изображение изначально было в формате AVIF, после оптимизации мы снизили вес картинки, а соответственно, и вес передаваемых с сервера данных более чем на 50 килобайт. 

На первый взгляд кажется, что это немного. Но вспомним, что страницы, как правило, с блоками. Например:

  • блок статей с их превью обычно содержит 3–4 статьи на видимом слайде;

  • блок отзывов с фото клиентов обычно содержит 2–3 отзыва на видимом слайде.

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

50 килобайт (из примера) * (3 статьи + 2 отзыва) = 250 килобайт. Звучит уже по-другому — сократили вес передаваемых данных на четверть мегабайта.

Пункт 2. Ленивая загрузка и предзагрузка

Всё, что видно на первом экране мобильной версии, грузим сразу (preload). Всё, что ниже, — с loading=»lazy» и decoding=»async». И обязательно проставляем ширину и высоту, чтобы не было скачков при загрузке.

Пример того, что должно получиться по итогу выполнения первых двух пунктов.

Head (например, если в бэкенде проверяется user-agent и отдаются булевы значения возможности использования AVIF/WEBP):    {% if page.use_avif %}    <link            rel="preload"            as="image"            href="{{ page.background.mobile.url_avif }}"            type="image/avif"            media="(max-width: 768px)">    <link            rel="preload"            as="image"            href="{{ page.background.tablet.url_avif }}"            type="image/avif"            media="(min-width: 769px) and (max-width: 1024px)">    <link            rel="preload"            as="image"            href="{{ page.background.desktop.url_avif }}"            type="image/avif"            media="(min-width: 1025px)">{% elif page.use_webp %}    <link            rel="preload"            as="image"            href="{{ page.background.mobile.url_webp }}"            type="image/webp"            media="(max-width: 768px)">    <link            rel="preload"            as="image"            href="{{ page.background.tablet.url_webp }}"            type="image/webp"            media="(min-width: 769px) and (max-width: 1024px)">    <link            rel="preload"            as="image"            href="{{ page.background.desktop.url_webp }}"            type="image/webp"            media="(min-width: 1025px)">{% else %}    <link            rel="preload"            as="image"            href="{{ page.background.mobile.url }}"            type="image/jpeg"            media="(max-width: 768px)">    <link            rel="preload"            as="image"            href="{{ page.background.tablet.url }}"            type="image/jpeg"            media="(min-width: 769px) and (max-width: 1024px)">    <link            rel="preload"            as="image"            href="{{ page.background.desktop.url }}"            type="image/jpeg"            media="(min-width: 1025px)">{% endif %}Для фото, которые спрятаны за первым экраном:<picture>        {% if property.preview.normal.srcset_avif %}                <source srcset="{{ property.preview.normal.srcset_avif }}" sizes="100vw" type="image/avif" media="(min-width: 1024px)">        {% endif %}        {% if property.preview.normal.srcset_webp %}                <source srcset="{{ property.preview.normal.srcset_webp }}" sizes="100vw" type="image/webp" media="(min-width: 1024px)">        {% endif %}            <source srcset="{{ property.preview.normal.srcset }}" sizes="100vw" type="image/jpeg" media="(min-width: 1024px)">        {% if property.preview.mobile.srcset_avif %}                <source srcset="{{ property.preview.mobile.srcset_avif }}" sizes="100vw" type="image/avif">        {% endif %}        {% if property.preview.mobile.srcset_webp %}                <source srcset="{{ property.preview.mobile.srcset_webp }}" sizes="100vw" type="image/webp">        {% endif %}            <source srcset="{{ property.preview.mobile.srcset }}" sizes="100vw" type="image/jpeg">            <img src="{{ property.preview.mobile.url }}"                 alt="{{ property.full_address() }}"                 loading="lazy"                 decoding="async"                 width="{{ property.preview.mobile.target_width }}"                 height="{{ property.preview.mobile.target_height }}">    </picture>

Итог этого пункта: 

  • побороли дергания страницы, учитываемые показателем CLS через установку атрибутов width/height;

  • ускорили первый экран, самый критичный для первичного посещения пользователем (preload);

  • убрали из начальной загрузки то, что пользователь при первом открытии сайта потенциально не будет видеть (lazy + async). 

Пункт 3. background-image

Картинка в стилях — не проблема. Скриптом определяем поддержку WebP/AVIF (например, через загрузку точки-картинки) — это те форматы, которые поддерживают основная часть браузеров. Далее добавляем класс, например, webp-allowed, к тегу <html>. В стилях через вложенные селекторы переключаем формат изображения в зависимости от формата и разрешения.

Пример того, что может получиться на языке SCSS:

banner {  // Мобильная версия (базовая, до 768px)  background-image: url('/static/img/banner-mobile.jpg');  .avif & {    background-image: url('/static/img/banner-mobile.avif');  }  .webp & {    background-image: url('/static/img/banner-mobile.webp');  }  // Планшет (768px и выше)  @media (min-width: 768px) {    background-image: url('/static/img/banner-tablet.jpg');.avif & {      background-image: url('/static/img/banner-tablet.avif');}.webp & {      background-image: url('/static/img/banner-tablet.webp');}  }  // Десктоп (1280px и выше)  @media (min-width: 1280px) {    background-image: url('/static/img/banner-desktop.jpg');.avif & {      background-image: url('/static/img/banner-desktop.avif');}.webp & {      background-image: url('/static/img/banner-desktop.webp');}  }}

Итог этого пункта: для background-image нельзя использовать <picture>, поэтому мы подложили поддержку форматов через класс на <html>. Скрипт проверил, с чем работает браузер (AVIF, WebP или только JPEG/PNG), добавил класс, а SCSS уже сам подставил правильный URL. В итоге фон на любом устройстве грузится в оптимальном формате, без лишнего веса.

Пункт 4. SVG

SVG — это векторная графика, она не должна весить мегабайты. Увидели подозрительно большой вес SVG, открываем файл в блокноте (или любом другом редакторе текста). Если внутри data:image/jpeg;base64, значит, дизайнер просто воткнул растровую картинку. Идём к нему и договариваемся о нормальном векторе, содержащем лишь векторные примитивы.

Также лучше сразу собрать загружаемые по отдельности мелкие SVG в спрайт и подключать один файл, особенно если их много (соцсети, иконки разделов).

Пункт 5. Выдыхаем

Изображения сконвертированы, оптимизированы и нарезаны.

Пример баллов PageSpeed после оптимизации двух изображений

Пример баллов PageSpeed после оптимизации двух изображений
Пример оставшихся ошибок: проблема с LCP ушла вместе с проблемой изображений

Пример оставшихся ошибок: проблема с LCP ушла вместе с проблемой изображений

Это только начало, но в то же время — одна из самых весомых частей. Остальное (скрипты, шрифты, кэширование) — тема следующих статей.

Итак, качество сайта зачастую страдает в силу экономии ресурсов, и это видно далеко не сразу. Технологии не стоят на месте: постоянно вводятся новые доработки в расчеты баллов и совершенствуются методы сжатия. Если не решать проблемы сразу, в дальнейшем они превратятся из периодических мелких задач в одну крупную, которую будет тяжело править без вреда контенту и затрат времени. Задача разработчика, продумать риски заранее.

Напоследок — краткий чек-лист, что можно сделать в будущем, чтобы заложить оптимизацию заранее:

  • Договориться с дизайнером — отдавать иконки в SVG (настоящем, без base64), а растровые изображения — в максимальном разрешении не больше 1920px (или нужном для макета).

  • Настроить автоматическую обработку загрузок — ресайз, конвертацию в WebP/AVIF, например, в рамках Django можно использовать сжатие через Pillow прямо в момент заливки в админке.

  • Использовать <picture> и srcset с рождения — не заменять потом, а сразу верстать с адаптивными картинками.

  • Заложить width/height для всех img — даже если кажется, что «потом проставим».

  • Сделать спрайты для мелких SVG (иконки, соцсети) — одним файлом, а не десятками запросов, так как каждый запрос — это обращение к серверу за данными и трата его ресурсов. Чем меньше обращений, тем меньше на него нагрузка и больше ресурсов на других клиентов.

Автор статьи — Михаил Клинов.


НЛО прилетело и оставило здесь промокод для читателей нашего блога:-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.

ссылка на оригинал статьи https://habr.com/ru/articles/1045832/