До и после: оптимизация изображений для Lighthouse и не только

Привет, интернет!  

Я Антон Небыков, Frontend TechLead в ИдаПроджект.

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

На первый взгляд, работа с изображениями сводится к простому добавлению элемента img и ссылки на изображение в атрибуте src. Но на практике все намного сложнее:) 

Погнали разбираться!

Оглавление

→ Проблемы, которые нужно было решить

→ Что получилось в итоге

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

→ Адаптивные изображения

→ Двухэтапная ленивая загрузка

→ Предварительная загрузка изображений

→ Результаты в Lighthouse

→ Заключение

Проблемы, которые нужно было решить

  1. Неоптимальные размеры изображений

  2. Неоптимальное сжатие изображений

  3. Отсутствие адаптивных изображений

  4. Отсутствие превью

  5. Неудобное добавление изображений в preload

  6. Низкие баллы в Lighthouse из-за неоптимизированных изображений

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

Вообще проблем, конечно, было гораздо больше, чем я написал, и часть из них связаны с особенностями работы SSR.

Например, для улучшения показателей в Lighthouse, мы используем ленивую гидратацию, однако ее применение влечет за собой ряд «побочных» эффектов. Вот лишь один из них: любой код, выполняемый на клиенте, будет запускаться только после гидратации — либо по таймауту, либо при взаимодействии пользователя с сайтом. Соответственно, если формирование нужных атрибутов для изображений (например генерацию sizes, srcset, ленивую загрузку и т.д.) реализовать на клиент, то изображение не появится до завершения процесса. 

Эта особенность справедлива только для первой загрузки сайт и первого экрана. Мы реализовали двухэтапную ленивую загрузку изображений (об этом расскажу попозже), и ее реализация предполагает исполнение кода — в том числе на клиенте. На первом экране ленивая загрузка не требуется — наоборот, надо как можно скорее отобразить изображения, поэтому мы добавили пропс no-lazy к компоненту VImage, который отключает ленивую загрузку.

Что получилось в итоге

На данный момент мы реализовали следующий функционал:

  • Интеграция с ImgProxy: для сжатия в WebP и отдачи любого нужного размера на лету

  • Двухэтапная загрузка изображений (плюс нативная через пропс): для улучшения показателей Lighthouse

  • Адаптив изображений под любой экран, в том числе экраны с высокими PPI

  • Превью изображений

  • Предварительная загрузка изображений (preload)

  • Улучшение показателей в Lighthouse

Все эти фичи входят в состав npm-пакета, который подключается ко всем новым проектам. Упаковка в пакет позволяет централизованно и очень быстро обновлять реализацию везде и сразу.

Для конечного пользователя (разработчика) все сводится к использованию компонента VImage с нужными пропсами.

Дальше расскажу подробнее о некоторых аспектах реализаций.

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

Чтобы изображения всегда были нужного размера, веса и качества, используем связку @nuxt/image и ImgProxy.

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

@nuxt/image — позволяет удобно связать наше приложение с ImgProxy или аналогичными сервисами. Для этого нужно разработать провайдер, который будет преобразовывать пропсы изображения в понятный ImgProxy формат. Использование @nuxt/image дает возможность в будущем заменить ImgProxy на другой сервис с минимальными временными затратами.

Пример использования:

На входе:

На выходе:

export default defineAppConfig({
    images: {
        breakpoints: {
            mobile: 0,
            tablet: 768,
            laptop: 1280,
            desktop: 1440,
            ultra: 1921,
        }
    }
});

Пример использования:

На входе:

На выходе:

Обратите внимание на атрибут src: в нем лежит наше превью, а именно, размытое блюром (bl:30) и уменьшенное (dpr:0.5) изображение в низком качестве (q:20).

Двухэтапная ленивая загрузка

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

Суть метода в том, что сначала мы показываем пустое изображения, после чего с помощью vanilla-lazyload подгружаем его легковесное превью и только потом — само изображение.

Делается это путем добавления data-атрибутов с превью и исходным изображением, которые последовательно подменяют значение атрибута src. Что касается адаптивных изображений, то там все аналогично: sizes и srcset спрятаны в data-атрибутах до момента попадания изображения в экран.

На входе:


До попадания в экран:

src — пустой gif

data-src — превью

data-lazy-src — конечное изображение

После попадания в экран и загрузки превью:

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

Поведение ленивой загрузки можно изменить; для этого у компонента VImage есть два пропса: two-steps и native-lazy.

two-steps — позволяет выключить двухэтапную загрузку.

native-lazy — включает (для изображения и его превью) коробочный loading=«lazy» и отключает двухэтапную загрузку.

Коробочным loading=«lazy» мы пользуемся только в специфических случаях. По нашим наблюдениям его работа не всегда так прозрачна, как хотелось бы. Однако мы реализовали его поддержку, поскольку надеемся, что в будущем браузеры явно улучшат работу.

Предварительная загрузка изображений

Если требуется чтобы изображение загрузилось как можно скорее (например, если оно находится на первом экране), то можно добавить на него ссылку в head.


  

    

   

  

    ...

  












В масштабах средних и крупных проектов ручное добавление ссылок в head на каждой странице не очень удобное занятие. Чтобы автоматизировать процесс добавление ссылок на изображения в head, мы воспользовались встроенным в Nuxt useHead.

Для разработчика требуется добавить пропсы no-lazy и preload.

no-lazy — выключает ленивую загрузку

preload — добавляет ссылку на изображение (и его превью) в head

Для адаптивных изображений все аналогично: в ссылке добавляются imagesrcset и imagesizes.

Пример использования:

Результаты в Lighthouse

На показатели в Lighthouse влияют множество факторов.

Использование адаптивных изображений вкупе с двухэтапной ленивой загрузкой приносят по нашим тестам от 5 до 30 баллов. Все очень зависит от количества изображений на странице и блоков на ней.

Мы остались довольны результатом — особенно в сравнении с результатами похожих проектов.

Также на проектах мы применяли другие техники для ускорения загрузки: ленивую гидратацию (я говорил о ней в самом начале статьи), «правильную» загрузку внешних скриптов и т.д. Это несколько выходит за рамки моей статьи, но если будет интерес, напишу про эти техники отдельный материал — кидайте запросы в комментарии :) 

Пример результатов

До оптимизации:

image.png
image.png

После:

image.png
image.png

Заключение

Я рассмотрел основные моменты работы с изображениями на проектах ИдаПроджект. Однако методы постоянно совершенствуется, мы постоянно ищем новые, более оптимальные решения существующих проблем.

Для некоторых простых проектов реализация всех вышеперечисленных техник избыточна и требует ресурсов на ее реализацию. Я не призываю использовать их все, но часть из них можно взять хотя бы для сокращения времени на разработку (долой рутинный труд разработчиков и контент-менеджеров!).

В чем преимущество нашей реализации: простота использования и подключения к новым проектам. От разработчика потребуется только подключить npm-пакет к своему Nuxt проекту (если он еще не подключен), и использовать компонент VImage с различными вариациями пропсов. Изи!

На этом у меня все:)

P.S. Что мы используем: Nuxt 3, ImgProxy, Vanilla-lazyload, @nuxt/image, nuxt-delay-hydration и Verdaccio.

© Habrahabr.ru