Как стать автором
Обновить
112.84
ИдаПроджект
Proptech разработчик №1

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

Уровень сложностиПростой
Время на прочтение8 мин
Количество просмотров2.1K

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

Я Антон Небыков, 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 на другой сервис с минимальными временными затратами.

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

На входе:

<VImage
    :width="800"
    image="https://test.ru/images/test.jpg"
/>

На выходе:

<img src="https://test.ru/proxy/w:800/q:80/plain/https://test.ru/images/test.jpg@webp>

То есть в итоге мы получаем ссылку на изображения с нужными модификаторами. Ширина изображения width преобразовалось в w:800, сама ссылка на изображение стала модификатором, и добавился формат @webp. После перехода по ссылке мы получим webp изображения с шириной в 800px.

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

Если одно и то же изображение применяется на разных устройствах (телефон, ноутбук, десктоп), то для отображения оптимальных изображений можно использовать атрибуты sizes и srcset.

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

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

Для генерации sizes и srcset можно воспользоваться встроенным в @nuxt/image методом.

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

Но в любом случае встроенный в @nuxt/image метод генерации позволяет в «два клика» получать sizes и srcset с нужными модификаторами изображений. Он также принимает список брейпоинтов, которые можно вынести в настройки проекта (например, в app.config.ts) и синхронизировать с scss брейкпоинтами.

Пример настроек в файле app.config.ts:

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

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

На входе:

<VImage
  image="https://test.ru/images/test.jpg"
  sizes="mobile:100vw tablet:100vw laptop:600px desktop:800px"
/>

На выходе:

<img
  src="https://test.ru/proxy/q:20/bl:30/dpr:0.5/plain/https://test.ru/images/test.jpg@webp"
  sizes="(max-width: 767px) 100vw, (max-width: 1279px) 100vw, (max-width: 1439px) 600px, 800px"
  srcset="https://test.ru/proxy/w:768/q:80/plain/https://test.ru/images/test.jpg@webp 768w, https://test.ru/proxy/w:1536/q:80/plain/https://test.ru/images/test.jpg@webp 1536w, https://test.ru/proxy/w:1280/q:80/plain/https://test.ru/images/test.jpg@webp 1280w, https://test.ru/proxy/w:2560/q:80/plain/https://test.ru/images/test.jpg@webp 2560w, https://test.ru/proxy/w:600/q:80/plain/https://test.ru/images/test.jpg@webp 600w, https://test.ru/proxy/w:1200/q:80/plain/https://test.ru/images/test.jpg@webp 1200w, https://test.ru/proxy/w:800/q:80/plain/https://test.ru/images/test.jpg@webp 800w, https://test.ru/proxy/w:1600/q:80/plain/https://test.ru/images/test.jpg@webp 1600w"
>

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

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

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

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

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

На входе:

<VImage
  image="https://test.ru/images/test.jpg"
  :width="100"
/>


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

<img 
  src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" 
  data-src="https://test.ru/images/proxy/q:20/bl:30/dpr:0.5/plain/https://test.ru/images/test.jpg@webp" 
  data-lazy-src="https://test.ru/images/proxy/w:100/h:0/q:80/plain/https://test.ru/images/test.jpg@webp"
>

src — пустой gif

data-src — превью

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

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

<img src="https://test.ru/images/proxy/w:100/h:0/q:80/plain/https://test.ru/images/test.jpg@webp">

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

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

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

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

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

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

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

<html>
  <head>

    <link rel="preload" as="image" href="https://test.ru/proxy/w:100/q:80/plain/https://test.ru/images/test.jpg@webp">

   </head>

  <body>

    ...

  </body>

</html>

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

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

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

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

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

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

<VImage
  :width="900"
  preload
  no-lazy
  image="https://test.ru/images/test.jpg"
/>

Результаты в 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.

Теги:
Хабы:
Всего голосов 28: ↑28 и ↓0+30
Комментарии2

Публикации

Информация

Сайт
idaproject.com
Дата регистрации
Дата основания
2013
Численность
201–500 человек
Местоположение
Россия
Представитель
Egor