Ленивая загрузка изображений с помощью пользовательских директив Vue.js и Intersection Observer

Roman Bushkoffsky
8 min readSep 22, 2018

Перевод статьи Mateusz Rybczonek: Lazy Loading images with Vue.js directives and IntersectionObserver

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

Сегодня изображения (их размер и количество на отдельно взятой странице) могут быть главной проблемой, когда дело касается производительности. Учитывая что от загрузки страницы напрямую зависит конверсия веб-сайта, эту проблему нельзя оставлять без внимания.

В этой статье я хотел бы поделиться одним из способов уменьшить начальный вес веб-сайта. Я покажу как загрузить пользователю только видимый контент когда он/она видит первый экран и лениво загрузить остальные “тяжелые” элементы (такие как изображения) когда они понадобятся.

Чтобы это сделать, нам необходимо решить:

Как хранить ссылки изображений не загружая их сразу.

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

Мы решим эти вопросы используя data-* атрибуты, Intersection Observer и пользовательские директивы Vue.js.

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

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

ImageItem.vue

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

Логика ленивой загрузки находится в директиве LazyLoadDirective которая использована в нашем компоненте с помощью добавления атрибута v-lazyload

Script этого компонента выглядит так:

ImageItem.vue

Как я упоминал раньше, логика ленивой загрузки содержится в директивеLazyLoadDirective:

LazyLoadDirective.js

Это только часть нашего примера, вы можете посмотреть его целиком на CodeSandbox. Далее шаг за шагом мы разберем весь код и посмотрим что происходит на самом деле.

Пример из реального мира: Создание директивы LazyLoadDirective и её использование в компоненте ImageItem

1. Создание основы для компонента ImageItem

Давайте начнем с создания компонента, который будет показывать наше изображение (пока без ленивой загрузки). Здесь мы создаем тег figure который содержит картинку, сама картинка получает атрибут src в котором находится ссылка на изображение(URL).

ImageItem.vue

В части script мы получаем свойство source, которое даёт нам URL картинки.

ImageItem.vue

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

2. Не даём картинке загрузиться сразу после создания компонента.

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

По определению data-* атрибут позволяет нам хранить информацию в соответствии со стандартном семантики HTML элементов.

Идеально подходит для нашего случая.

ImageItem.vue

Хорошо, так наше изображение не загрузится. Но подождите, оно не загрузится… никогда!

Очевидно что это не то что мы хотели, нам необходимо чтобы изображение загрузилось с определенным условием. Мы можем запросить картинку заменив значение атрибута src значением атрибута data-url, содержащим ссылку на изображение. Это не составит труда, но проблема в том, когда же нам следует сделать это?

Мы хотим загрузить изображение, когда компонент, который её содержит станет видимым для пользователя. Как мы можем определить видит пользователь картинку или нет? Узнаем об этом далее.

3. Определяем, видит ли пользователь изображение.

Если вы когда-нибудь встречались с такой задачей, вы, вероятно, использовали магию JavaScript’а, которая, в конце-концов выглядела чудовищно…

Например, мы могли использовать события и функции отслеживающие позицию скролла, значение отступа, высоту элемента и высоту вьюпорта чтобы вычислить находится ли картинка в нём или нет. Звучит безумно, не так ли?

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

Это безумие!

4. Intersection Observer как спасение.

Такой очень неэффективный способ определить видит ли пользователь элемент или нет можно заменить используя Intersection Observer API.

Судя по описанию он позволяет назначить коллбэк-функцию которая будет выполняться каждый раз когда целевой элемент пересекает вьюпорт устройства или другой определенный элемент.

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

Итак, что нам необходимо?

Чтобы использовать Intersection Observer нам необходимо:

Создать Intersection Observer

Отслеживать элемент который мы хотим лениво загрузить при изменении видимости.

Когда элемент находится во вьюпорте, загрузить его (поменять значение атрибута src на значение в data-url)

Когда элемент загрузится, прекратить его отслеживание (unobserve)

Во Vue JS мы можем использовать пользовательскую директиву чтобы обернуть весь этот функционал и переиспользовать когда это необходимо.

5. Создание пользовательской директивы.

Что такое пользовательская директива? Как гласит документация, это способ для низкоуровневого доступа к DOM элементу. Например, для изменения атрибута элемента, в нашем случае — атрибута src элемента img.

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

LazyLoadDirective.js

Начнём, шаг за шагом.

hookFunction

Позволяет нам выполнять действия в определенный момент жизненного цикла элемента.

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

LazyLoadDirective.js

функция loadImage

Отвечает за замену значения атрибута src значением атрибута data-url

В этой функции мы получаем доступ к el, элементу к которому применена директива. Мы можем получить доступ к img из этого элемента.

Мы проверяем существует ли изображение, и если да, то добавляем слушателя, который будет вызывать коллбэк-функцию когда загрузка завершена. Коллбэк отвечает за скрытие спиннера и добавление анимации появления (эффект плавного появления) к картинке используя CSS класс.

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

И в конце мы меняем значение атрибута src нашего на ссылку на картинку.

LazyLoadDirective.js

функция handleIntersect

Коллбэк-функция IntersectionObserver’а отвечает за вызов loadImage с несколькими условиями.

Она будет вызвана когда IntersectionObserver определит что сам элемент или его родительский компонент появился во вьюпорте.

У неё есть доступ к entries который является массивом элементов за которыми наблюдает observer и сам observer.

Мы перебираем массив entries и проверяем не появился ли в зоне видимости один из наблюдаемых элементов (свойство isIntersecting), если это случилось то вызываем функцию loadImage.

После запроса картинки, мы перестаем следить за элементом (метод unobserve), это избавит нас от повторной загрузки изображения.

LazyLoadDirective.js

функция createObserver

Отвечает за создание IntersectionObserver’а и прикрепление его к нашему элементу.

Конструктор IntersectionObserver принимает коллбэк-функцию (наша handleIntersect функция) которая вызывается когда элемент пересекает определенный порог (threshold) и объект с настройками нашего observer’а.

Объект настроек содержит root — ссылку на элемент относительно которого отслеживается видимость объекта (это может быть любой предок или вьюпорт браузера, если мы передаём null). Также устанавливаем значение threshold которое варьируется от 0 до 1 которое означает на сколько процентов должен быть видим наблюдаемый элемент относительно объекта root чтобы была вызвана функция-коллбэк (0 — при появлении хотя бы одного пикселя, 1 — видим должен быть весь элемент).

После создания IntersectionObserver мы прикрепляем его к нашему элементу используя метод observe.

LazyLoadDirective.js

Браузерная поддержка

Поддерживается пока не всеми браузерами, покрытие 73% пользователей (на 28 августа 2018) звучит не так уж и плохо.

Но понимая что мы хотим чтобы все пользователи могли увидеть наше изображение (помним, что использование data-url предотвращает загрузку изображения), необходимо добавить ещё несколько строк кода к нашей директиве.

Мы проверим поддержку IntersectionObserver’а браузером, и если она отсутствует, то загрузим все изображения сразу вызовом функции loadImage или вызовем createObserver если браузер поддерживает этот API.

LazyLoadDirective.js

6. Регистрация директивы

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

Глобальная регистрация

Чтобы зарегистрировать директиву глобально мы импортируем её и используем метод Vue.directive передавая имя под которым хотим её зарегистрировать и саму директиву. Это позволит нам использовать атрибут v-lazyload на любом элементе в нашем коде

main.js

Локальная регистрация

Если мы хотим использовать нашу директиву только в отдельном компоненте, мы можем зарегистрировать её локально. Чтобы это сделать мы импортируем нашу директиву внутри компонента в котором мы хотим её использовать и регистрируем её в объекте directives. Это даст нам возможность добавить v-lazyload атрибут к любому элементу внутри компонента.

7. Использование директивы в компоненте ImageItem.

После регистрации директивы мы можем использовать её, добавив атрибутv-lazyloadк элементу-родителю который содержит img (в нашем случае тегfigure).

ImageItem.vue

Подводя итоги

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

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

Давайте остановимся на наших 11 изображениях и проверим производительность страницы на быстром 3G без использования ленивой загрузки.

Как и ожидалось, 11 изображений, 11 запросов, общий размер страницы 3.2 MB

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

В результате 1 изображение, 1 запрос, общий размер страницы 1.4 MB

С помощью добавления этой директивы к нашим статьям мы сократили количество запросов на 10, а размер страницы на 56% и это ещё достаточно простой пример.

Думаю комментарии будут излишни, цифры говорят сами за себя.

--

--