До и после: оптимизация изображений для Lighthouse и не только
Привет, интернет!
Я Антон Небыков, Frontend TechLead в ИдаПроджект.
Хочу поделиться опытом автоматизации отображения изображений на сайте, их оптимизацией и — как следствие — улучшением показателей в Lighthouse.
На первый взгляд, работа с изображениями сводится к простому добавлению элемента img и ссылки на изображение в атрибуте src. Но на практике все намного сложнее:)
Погнали разбираться!
Оглавление
→ Проблемы, которые нужно было решить
→ Что получилось в итоге
→ Оптимизация размеров и сжатие изображений
→ Адаптивные изображения
→ Двухэтапная ленивая загрузка
→ Предварительная загрузка изображений
→ Результаты в Lighthouse
→ Заключение
Проблемы, которые нужно было решить
Неоптимальные размеры изображений
Неоптимальное сжатие изображений
Отсутствие адаптивных изображений
Отсутствие превью
Неудобное добавление изображений в preload
Низкие баллы в 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 баллов. Все очень зависит от количества изображений на странице и блоков на ней.
Мы остались довольны результатом — особенно в сравнении с результатами похожих проектов.
Также на проектах мы применяли другие техники для ускорения загрузки: ленивую гидратацию (я говорил о ней в самом начале статьи), «правильную» загрузку внешних скриптов и т.д. Это несколько выходит за рамки моей статьи, но если будет интерес, напишу про эти техники отдельный материал — кидайте запросы в комментарии :)
Пример результатов
До оптимизации:

После:

Заключение
Я рассмотрел основные моменты работы с изображениями на проектах ИдаПроджект. Однако методы постоянно совершенствуется, мы постоянно ищем новые, более оптимальные решения существующих проблем.
Для некоторых простых проектов реализация всех вышеперечисленных техник избыточна и требует ресурсов на ее реализацию. Я не призываю использовать их все, но часть из них можно взять хотя бы для сокращения времени на разработку (долой рутинный труд разработчиков и контент-менеджеров!).
В чем преимущество нашей реализации: простота использования и подключения к новым проектам. От разработчика потребуется только подключить npm-пакет к своему Nuxt проекту (если он еще не подключен), и использовать компонент VImage с различными вариациями пропсов. Изи!
На этом у меня все:)
P.S. Что мы используем: Nuxt 3, ImgProxy, Vanilla-lazyload, @nuxt/image, nuxt-delay-hydration и Verdaccio.