Lazy-load: ленивая загрузка

Асинхронная загрузка изображений и фреймов позволяет существенно увеличить скорость отображения веб-страницы и сэкономить трафик пользователей, показывая те или иные элементы лишь при наступлении определенных разработчиком событий. Так называемый lazy-load пользуется популярностью на сайтах, где преобладает большой по весу медиа-контент, состоящий, например, из множества картинок. Несмотря на кажущуюся сложность, реализовать собственный скрипт «ленивой» загрузки элементов средствами «vanilla» JS достаточно просто.

Принцип работы скриптов lazy-load

Lazy-load или «ленивая» загрузка — это способ отображения контента на сайте, когда элементы, появление которых определяет внешний ресурс, загружаются не сразу вместе с запрашиваемой страницей, а асинхронно — по мере необходимости. К подобным элементам относятся, например, изображения (<img>) и фреймы (<iframe>). Их объединяет наличие атрибута src, указывающего на источник ресурса.

Когда браузер «видит» у элемента атрибут src, то осуществляет сетевой запрос по указанному адресу, что увеличивает время полной загрузки документа. Соответственно, чем больше внешних ресурсов синхронно подключается к странице, тем медленнее она загружается. Чтобы избежать множества одновременных запросов и оптимизировать скорость загрузки, используется техника lazy-load.

Пример загрузки страницы со множеством изображений без lazy-load Пример загрузки страницы со множеством изображений без lazy-load
Большое количество осуществляемых одновременно запросов к внешним ресурсам способно значительно увеличить время загрузки страницы, заставляя пользователя ждать.

Основы работы большинства скриптов «ленивой» загрузки описываются подобным алгоритмом:

  1. Элементы, которые необходимо загружать асинхронно, встраиваются на страницу с другим, временным значением атрибута src (или вовсе без него, что неправильно с точки зрения валидации кода). Как правило, временное значение src содержит адрес шаблона, который по весу на порядок легче оригинального исходника.
  2. При необходимости в атрибут src вставляется оригинальный, исходный адрес ресурса. Как правило, эта замена может осуществляться двумя путями:
    1. По требованию пользователя, когда для элемента наступают события click или hover.
    2. Автоматически при попадании элемента в пользовательский viewport путём проверки события scroll, а также resize и orientationchange для мобильных устройств.

Таким образом, при использовании lazy-load при первой загрузке страницы производится не одновременно несколько запросов к разным внешним ресурсам, а всего один — к временному шаблону, указанному в атрибуте src, который кэшируется браузером, и только затем при необходимых условиях (наступление заданных событий) для выбранных элементов устанавливается их оригинальный src.

Существует и иной подход, при котором можно обойтись без замещающих шаблонов для оригинальных ресурсов: элементы не встраиваются на страницу изначально, а создаются динамически в процессе просмотра или определенных действий пользователя (подобные манипуляции с DOM используются, например, при подключении скриптов статистики Google Analytics или Яндекс. Метрики) либо клонируются из Shadow DOM (по такому принципу работает элемент <content>).

Асинхронная загрузка картинок и фреймов

«Презентацию» скрипта lazy-load следует начать с некоторых разъяснений. Во-первых, для правильной работы скрипт должен запускаться после полной загрузки HTML и постройки DOM-дерева, т. е. наступления события DOMContentLoaded. Во-вторых, в HTML-разметке у элементов, которые планируется загружать асинхронно, будет присутствовать атрибут data-js-source, указывающий на оригинальный источник ресурса, — с его помощью происходит поиск элементов, для которых необходимо инициализировать lazy-load, поэтому, чтобы отложенная загрузка заработала на желаемом элементе, к нему достаточно будет добавить указанный атрибут с источником. В-третьих, при использовании скрипта следует заранее позаботиться о шаблонах, которые станут отображаться взамен оригинального src элементов. Наконец, настройки скрипта учитывают варианты отображения оригинального контента — по требованию пользователя и автоматически, для чего применяются описанные выше события: click, hover — непосредственно на самих элементах, а scroll, resize и orientationchange — для окна браузера, если требуется отследить попадание во viewport.

JavaScript

В качестве единственного аргумента в метод init() функции «ленивой» загрузки должен передаваться объект с предварительными настройками, содержащий:

  1. название селектора, по которому через document.querySelectorAll выбираются необходимые элементы;
  2. указание на событие, активирующее замену атрибута src на оригинальный, — click, hover или view (попадание во viewport);
  3. callback-функцию, вызываемую во время загрузки источника.
{
  items:    "[data-js-source]", // селектор элементов lazy-load
  on:       "view",             // click, view или hover
  callback:  false,             // функция, вызываемая во время загрузки источника
}

Скрипт представлен в самом тривиальном исролнении и работает по следующей схеме: выборка всех необходимых элементов на странице, проверка их на наличие атрибута src и источника оригинального ресурса (атрибут data-js-source), установка событий на окно или на сами элементы в зависимости от выбранного способа отображения контента, замена src у конкретного элемента на содержимое в data-js-source при наступлении соответствующих событий. Для удобства добавлено информирование через консоль браузера об ошибках, возникающих при поиске элементов, при добавлении элемента без атрибута src, при отсутствии или неудачной загрузке (событие error) оригинального источника. Если на сайте есть картинки, которые могут не загрузиться из-за ошибки в источнике, рекомендуется также добавить CSS для «сломанных» изображений.

/**
 * Анонимная самовызывающаяся функция-обертка
 * @param {document} d - получает документ
 */
!function(d) {

  "use strict";

  /**
   * Полифилл для Object.assign()
   */
  Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:function(e,r){"use strict";if(null==e)throw new TypeError("Cannot convert first argument to object");for(var t=Object(e),n=1;n= 0 && coords.left >= 0 && coords.top) <= (window.innerHeight || d.documentElement.clientHeight);
  };

  /**
   * Функция загрузки элемента
   * @param {Event|Object} e - событие получаемое при click/hover или элемент, если используется "view"
   * @param {Function|false} [callback] - функция, выполняемая после загрузки элемента
   */
  lazy.get = function(e, callback) {
    var item = e.target || e;
    var event = e.type || false;
    if (event) item.removeEventListener(event, lazy.get); // удаление обработчика события
    if (!item.src) { // проверка элемента на наличие атрибута src
      console.error("lazy-load не поддерживается для этого элемента (" + item.tagName + ")");
      return;
    }
    if (!item.hasAttribute("data-js-loaded")) {
      var src = item.getAttribute("data-js-source");
      if (src) { // если атрибут data-js-source присутствует
        item.src = src;
        item.setAttribute("data-js-loaded", "");
        item.removeAttribute("data-js-source");
        item.onerror = function() { // обработка ошибок загрузки
          console.error("загрузка источника не удалась (элемент: " + item.tagName + ", путь: " + item.src + ")");
        };
        setTimeout(function() { // удаление обработчика ошибок
          item.onerror = null;
        }, 3000);
      } else console.error("отсутствует оригинальный источник (элемент: " + item.tagName + ", путь: " + item.src + ")");
      if (callback) callback(item); // вызов callback, если необходимо
    }
  };




  /**
   * Функция инициализации lazy-load
   * @param {Object} settings - объект с предварительными настройками
   */
  lazy.init = function(settings) {

    // настройки по умолчанию
    settings = Object.assign({}, {
      items:    "[data-js-source]",
      on:       "click",
      callback: false,
    }, settings);

    // поиск всех lazy-элементов
    var els = d.querySelectorAll(settings.items);


    // обработчики событий
    if (settings.on === "view") { // для события с условным наимнованием "view"
      var onviewLoad = function() {
        Array.prototype.slice.call(els, 0).forEach(function(el) {
          if (lazy.isVisible(el)) lazy.get(el, settings.callback); // проверка на видимость во viewport, lazy.get() принимает элемент
        });
      };

      // обработка событий для "view"
      window.addEventListener("scroll", onviewLoad);
      window.addEventListener("resize", onviewLoad);
      if (screen.msOrientation || (screen.orientation || screen.mozOrientation || { type: false }).type) window.addEventListener("orientationchange", onviewLoad);
    } else { // обработка событий "hover" и "click"
      var eventName = (settings.on === "hover") ? "mouseover" : settings.on;
      Array.prototype.slice.call(els, 0).forEach(function(el) {
        el.addEventListener(eventName, function(e) {
          lazy.get(e, settings.callback);
        }); // lazy.get() принимает событие
      });
    }

  };

  window.lazy = lazy;

}(document);

Элемент, который еще не загрузился и содержит в своем src шаблон, обладает атрибутом data-js-source. При загрузке оригинального источника этот атрибут удаляется и добавляется новый — data-js-loaded. Эти селекторы могут быть использованы для стилизации элементов.

CSS-оформление

Чтобы добавить к lazy-элементам CSS, можно указать следующие селекторы:

/* все не загрузившиеся элементы  */ 
[data-js-source] { }

/* все загрузившиеся элементы */
[data-js-loaded] { }

/* все не загрузившиеся фреймы */
iframe[data-js-source] { }

/* все не загрузившиеся картинки */
img[data-js-source] { }

/* все загрузившиеся фреймы */
iframe[data-js-loaded] { }

/* все загрузившиеся картинки */
img[data-js-loaded] { }

Благодаря callback-функции к загружаемой картинке можно добавить класс и настроить эффект появления на свой вкус. В примере ниже это реализовано посредством opacity и transition.

Пример асинхронной загрузки

Миниатюрный скрипт lazy-load обладает широкой кроссбраузерностью. Для поддержки Internet Explorer 9+ и Opera 12 Presto следует добавить полифилл для метода Object.assign():

В заключение

«Ленивая» загрузка изображений и фреймов — это популярная техника для улучшения производительности веб-страниц, которая часто встречается в социальных сервисах, интернет-магазинах, галереях и всех прочих сайтах, где медиа-контент занимает большую часть веса документа. Представленный вниманию читателя скрипт lazy-load выполняет схожую функцию — асинхронно загружает контент при наступлении указанного события и вполне подходит для использования на небольших сайтах или ознакомления с возможным способом реализации отложенной загрузки элементов.