Vue.js и Intersection Observer
Недавно просмотренный ролик Вадима Макеева с аудитом сайта lingualeo заставил задуматься о том, что изображения действительно могут зачастую составлять бОльшую часть трафика. Это может быть пост в блоге с большим количеством картинок, лента новостей и т.д. и т.п. И загрузка множества изображений может существенно снизить производительность сайта особенно на мобильных устройствах. Плюс есть ещё такой момент: если картинка находится где-то там внизу за пределами вьюпорта, то не факт, что пользователь вообще до неё доберётся. И получится, что картинку он загрузил, но не увидел. А зачем же грузить то, что не нужно?
В общем, если у вас есть какой-то контент с большим количеством картинок, то будет очень неплохо использовать технику «ленивая загрузка». Именно этим мы сейчас и займёмся.
Что за ленивая загрузка?
Ленивая загрузка означает, что мы не загружаем изображение до тех пор, пока оно не понадобится. Кстати, так можно делать не только с изображениями. Можно и javascript так загружать, но мы остановимся только на изображениях. Когда же наступает тот момент, в который необходимо начинать загрузку изображения? Очень просто — когда предполагаемое изображение попадает во вьюпорт. Отлично, теперь осталось только поймать этот момент. Мы оставим в стороне жуткие способы с кучей расчетов — они некрасивые и плохо работают. Мы же хотим, чтобы было красиво и хорошо. Поэтому будет использовать Intersection Observer.
Что ещё за Observer?
Это новый браузерный API, который делает именно то, что нам нужно: следит когда элемент попадает во вьюпорт (или пересекается с любым указанным элементом) и готов в этот момент выполнить любую функцию по нашему желанию. Да, браузерная поддержка оставляет желать лучшего. Только пользователи Chrome, Edge и Firefox смогут оценить наши усилия. Но у нас нет цели осчастливить всех, правильно? Мы используем новую технологию, которая облегчит жизнь прогрессивным пользователям. Остальные же будут как и раньше загружать сразу все картинки. Так что всё ок.
Пройдёмте в закрома
Ну хватит разговоров. Давайте уже сделаем что-нибудь на практике. Так как я в основном делаю проекты на Vue.js, то его мы и будем использовать для примера. Пример возьмём прямо из жизни. В проекте, над которым я сейчас работаю, есть раздел в административной части, в котором нужно показать все загруженные в систему изображения. Представляете, да? Страница, на которой 25/30/50 изображений, в зависимости от выбора пользователя. Отличный повод применить Intersection Observer. Для отображения картинок я создам компонент ImageItem.
<template> <div class="image"> <img class="image__item" :src="source" alt="" > </div></template>
Ну тут всё просто и очевидно. Самый обычный тэг <img>
, атрибут src которого является входным параметром (aka props) компонента. Раздел script соответственно выглядит так:
export default { name: "", props: { source: { type: String, required: true } }}
В таком виде наш компонент просто будет показывать изображение как обычно. Для ленивой загрузки нам для начала нужно предотвратить загрузку изображений в принципе. Да, чтобы они вообще не загружались. Ведь мы хотим загружать изображение только тогда, когда оно попадает во вьюпорт. И самый простой способ предотвратить загрузку изображения — убрать у него атрибут src. Но нам же в любом случае понадобится адрес, с которого грузить изображение. Т.е. мы всё равно должны как-то сохранить наш входной параметр source. Для этого отлично подойдёт data атрибут. И шаблон компонента будет выглядеть так:
<template> <div class="image"> <img class="image__item" :data-src="source" alt="" > </div></template>
Ну что ж, пол дела сделано. Изображения не загружаются. Осталось всего ничего — при попадании изображения во вьюпорт вернуть ему атрибут src и подставить в него значение из data-src. Получив атрибут src изображение сразу же загрузится. И вот тут наконец-то на сцену выходит наш герой. Так как мы используем Vue.js, то логичнее всего оформить ленивую загрузку в виде пользовательской директивы. Создадим папку directives
, а в ней файл lazyImages.js
:
export default { inserted: el => { function loadImage() { const imageElement = Array.from(el.children) .find(el => el.nodeName === "IMG"); if (imageElement) { imageElement.addEventListener("error", () => console.log("error")); imageElement.src = imageElement.dataset.src; } } function handleIntersect(entries, observer) { entries.forEach(entry => { if (entry.isIntersecting) { loadImage(); observer.unobserve(el); } }); } function createObserver() { const options = { root: null, threshold: "0" }; const observer = new IntersectionObserver(handleIntersect, options); observer.observe(el); } if (window["IntersectionObserver"]) { createObserver(); } else { loadImage(); } }};
Теперь разберём по частям, что происходит. У пользовательской директивы во Vue, как и у компонента, есть хуки. Мы функционал нашей директивы вешаем на хук inserted
, так как нам необходимо, чтобы элемент уже присутствовал в DOM.
В функции loadImage
мы просто возвращаем изображениям атрибут src, чтобы инициировать загрузку.
За запуск функции loadImage
, в свою очередь, отвечает функция handleIntersect
. В неё мы передаём набор элементов, за которыми ведётся наблюдение (entries) и экземпляр observer. Как только элемент попадает во вьюпорт (entry.isIntersecting), мы инициируем загрузку (imageLoad) и убираем его из списка наблюдения observer (observer.unobserve(el)).
Сам observer создаётся в функции createObserver
.
В итоге логика работы директивы получается такая. Первым срабатывает условие:
if (window["IntersectionObserver"]) { createObserver();} else { loadImage();}
Т.е. если браузер не поддерживает IntersectionObserver, то мы просто грузим картинки. Если же поддерживает, то мы вызываем createObserver
, которая устраивает слежку за нашими картинками и загружает их только тогда, когда они попадают во вьюпорт.
Регистрация директивы
Почти готово, осталось совсем немного — нужно зарегистрировать директиву. Для глобальной регистрации достаточно добавить её в файл main.js:
...import LazyLoadDirective from "./directives/lazyImages";Vue.directive("lazyload", LazyLoadDirective);...
Если же мы хотим зарегистрировать директиву локально (ну не нужна она другим компонентам), то в файле компонента прописываем:
import LazyLoadDirective from "./directives/lazyImages";export default { directives: { lazyload: LazyLoadDirective }}
Используем директиву
Ну вот наконец и добрались до момента применения директивы на практике. Тут всё предельно просто:
<template> <div class="image" v-lazyload> <img class="image__item" :data-src="source" alt="" > </div></template>
Ура, мы сделали это 🤘