Эта статья была вдохновлена ​​и основана на этом замечательном сообщении в блоге, написанном Бенджамином Тейлором.

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

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

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

Для этого нам нужно решить две вещи:

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

Вышеупомянутые проблемы мы решим с помощью атрибутов данных, IntersectionObserver и настраиваемой директивы Vue.js.

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

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

ImageItem.vue

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

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

Часть сценария этого компонента выглядит так:

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

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

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

1. Создайте базовый компонент ImageItem.

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

В части сценария мы получаем опору source, которая дает нам исходный URL-адрес отображаемого изображения.

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

2. Предотвратить загрузку изображения при создании компонента.

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

Согласно его определению, data- * attribute позволяет нам хранить информацию о стандартных, семантических HTML-элементах.

Похоже, идеально подходит для наших нужд.

Хорошо, после этого мы не будем загружать наше изображение. Но подождите, мы не загрузим наше изображение… никогда!

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

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

3. Определите, когда изображение видно пользователю.

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

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

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

Это безумие!

4. Наблюдатель перекрестков спешит на помощь!

Этот очень неэффективный способ определения, является ли элемент видимым в области просмотра, можно решить с помощью Intersection Observer API.

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

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

Итак, что нам нужно сделать, чтобы его использовать?

Чтобы использовать Наблюдатель пересечения, нам нужно сделать несколько вещей:

  • Создайте нового наблюдателя пересечения
  • Следите за элементом, который мы хотим отложить, чтобы изменить видимость.
  • Когда элемент находится в области просмотра, загрузите элемент (замените src нашим data-url)
  • Как только элемент загружен, перестаньте следить за ним на предмет изменений видимости (unobserve)

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

5. Создайте настраиваемую директиву Vue.

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

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

Пойдем шаг за шагом.

hookFunction

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

функция loadImage

  • отвечает за замену значения src на data-url.
  • в этой функции у нас есть доступ к нашему el, элементу, к которому применяется директива. Мы можем извлечь img из этого элемента.
  • мы проверяем, существует ли изображение, и если он существует, мы добавляем слушателя, который запускает функцию обратного вызова по завершении загрузки. Этот обратный вызов будет отвечать за скрытие счетчика и добавление анимации (эффект постепенного появления) к изображению с помощью класса CSS.
  • мы добавляем второй слушатель, который будет вызываться, когда загрузка изображения с нашего URL-адреса src не удалась.
  • Наконец, мы заменяем src нашего элемента img на исходный URL-адрес изображения, которое мы хотим запросить и показать (которое запускает запрос).

Функция handleIntersect

  • Функция обратного вызова IntersectionObserver, отвечающая за срабатывание loadImage при определенных условиях.
  • он запускается, когда IntersectionObserver обнаруживает, что элемент вошел в область просмотра или элемент родительского компонента.
  • он имеет доступ к entries, который представляет собой массив всех элементов, за которыми наблюдает наблюдатель, и observer сам.
  • мы перебираем entries и проверяем, становится ли отдельная запись видимой для нашего пользователя isIntersecting, если это так, loadImage функция запускается
  • после запроса изображения мы unobserve элемент (удаляем его из списка наблюдения наблюдателя), что предотвращает повторную загрузку изображения.

createObserver функция

  • отвечает за создание нашего IntersectionObserver и прикрепление его к нашему элементу.
  • Конструктор IntersectionObserver принимает обратный вызов (наша функция handleIntersect), который запускается, когда наблюдаемый элемент передает указанные threshold и параметры объект, который несет наши возможности наблюдателя.
  • options объект указывает root, что это наш эталонный объект, на котором мы основываем видимость наблюдаемого элемента (это может быть любой предок объекта или область просмотра нашего браузера, если мы передадим null). Он также указывает значение threshold, которое может варьироваться от 0 до 1, и сообщает нам, при каком проценте видимости цели должен выполняться обратный вызов наблюдателя (0 означает, что как только будет виден хотя бы один пиксель, 1 означает, что весь элемент должен быть виден).
  • после создания IntersectionObserver мы прикрепляем его к нашему элементу с помощью метода observe.

Поддержка браузера

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

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

Нам нужно проверить, поддерживает ли браузер IntersectionObserver, запустить loadImage, если он не поддерживает (запросит все изображения сразу), и createObserver, если он поддерживает.

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

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

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

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

Местная регистрация

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

7. Директива using для компонента ImageItem

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

Резюме

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

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

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

Как и ожидалось, 11 изображений, 11 запросов, общий размер страницы 3,2 МБ.

Теперь та же страница, на которой видна только первая статья и изображения с отложенной загрузкой.

Результат, 1 изображение, 1 запрос, общий размер страницы 1,4 МБ.

Добавив эту директиву в наши статьи, мы сохранили 10 запросов и уменьшили размер страницы на 56% и помним, что это действительно простой пример.

Больше никаких комментариев, пусть цифры говорят сами за себя.