FRONTEND BLOG

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>

Ура, мы сделали это 🤘