Справочники, инструменты, документация

JavaScript: Lazy Load 2.0 (ленивая отложенная загрузка изображений)

Чтобы не заставлять человека ждать и максимально быстро предоставить ему информацию, в которой он нуждается, и существует чудесный плагин под названием «Lazy Load». Он позволяет подгружать изображения по мере прокрутки страницы, когда они начинают попадать в область видимости.

Первые версии плагина, автором которого, кстати говоря, является Мика Туупола, были реализованы с использованием jQuery. В этой же статье я покажу вам свежую версию плагина (Remastered, как называет ее сам автор) на чистом JavaScript.

  1. Скачайте код в конце статьи, создайте js и загрузите на ваш сайт.

  2. Далее в секцию HEAD на всех страницах вашего сайта подключите ранее загруженный скрипт:

<script src="/js/lazyload.js"></script>

Не забывайте корректно прописывать адрес до скрипта на вашем сайте.

  1. Перед закрывающим тегом </body> вы вставляете скрипт вызова плагина и стиль для скрытия отложенных изображений при отключенном в браузере JavaScript:
<style>
    img[data-src] {
        display: none !important;
    }
</style>

<script>
    let images = document.querySelectorAll("img");
    lazyload(images);
</script>

img в скрипте означает, что «лениво» загружаться будут все изображения. Можно поставить ограничение в виде определенного CLASS или ID.

  1. Затем (не обязательно, но желательно), в секцию HEAD (обязательно в нее, а не в другое место или отдельный файл стилей) вставьте следующие правила:
<style>
    img[data-src] {
        opacity: 0 !important;
    }

    img[src] {
        opacity: 1 !important;
    }
</style>

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

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

  1. Пятый и он же, по сути, завершающий шаг. Здесь вам необходимо изменить принцип вставки изображения на сайт. Стандартная конструкция вставки изображения в HTML выглядит так:
<img class="logo_style" src="/logo-big.png" alt="Наш логотип" />

Различия с кодом ваших изображений могут быть лишь в разного рода атрибутах. Главное здесь то, что ранее используемый атрибут src вы заменяете на data-src:

<img class="logo_style" data-src="/logo-big.png" alt="Наш логотип" />

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

По желанию можно не удалять атрибут src и прописывать в нем ссылку до превью (уменьшенной копии):

<img class="logo_style" src="/logo-small.png" data-src="/logo-big.png" alt="Наш логотип" />

Автоматическое изменение кода изображений

Есть несложный способ, который позволяет автоматически изменить необходимый атрибут у изображений. Для этого на все страницы вашего сайта (если вы используете CMS, то в главный файл, чаще всего это файл index.php) в самый верх вставьте код:

function lazyload_img($buffer) {
    return preg_replace("#<img([^>]*) src=['\"](.*?)['\"]([^>]*)>#", '<img$1 data-src="$2"$3><img$1 src="$2"$3>', $buffer);
} 

ob_start("lazyload_img");

Обратите внимание: на вашем сайте должна быть поддержка PHP. Приведенный скрипт ищет все изображения на странице и заменяет им атрибут src на data-src.

lazyload.js

(function (root, factory) {
  if (typeof exports === "object") {
    module.exports = factory(root);
  } else if (typeof define === "function" && define.amd) {
    define([], factory(root));
  } else {
    root.LazyLoad = factory(root);
  }
}) (typeof global !== "undefined" ? global : this.window || this.global, function (root) {

  "use strict";

  const defaults = {
    src: "data-src",
    srcset: "data-srcset",
    selector: ".lazyload"
  };

  /**
  * Merge two or more objects. Returns a new object.
  * @private
  * @param {Boolean} deep   If true, do a deep (or recursive) merge [optional]
  * @param {Object}  objects The objects to merge together
  * @returns {Object}     Merged values of defaults and options
  */
  const extend = function () {

    let extended = {};
    let deep = false;
    let i = 0;
    let length = arguments.length;

    /* Check if a deep merge */
    if (Object.prototype.toString.call(arguments[0]) === "[object Boolean]") {
      deep = arguments[0];
      i++;
    }

    /* Merge the object into the extended object */
    let merge = function (obj) {
      for (let prop in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, prop)) {
          /* If deep merge and property is an object, merge properties */
          if (deep && Object.prototype.toString.call(obj[prop]) === "[object Object]") {
            extended[prop] = extend(true, extended[prop], obj[prop]);
          } else {
            extended[prop] = obj[prop];
          }
        }
      }
    };

    /* Loop through each object and conduct a merge */
    for (; i < length; i++) {
      let obj = arguments[i];
      merge(obj);
    }

    return extended;
  };

  function LazyLoad(images, options) {
    this.settings = extend(defaults, options || {});
    this.images = images || document.querySelectorAll(this.settings.selector);
    this.observer = null;
    this.init();
  }

  LazyLoad.prototype = {
    init: function() {

      /* Without observers load everything and bail out early. */
      if (!root.IntersectionObserver) {
        this.loadImages();
        return;
      }

      let self = this;
      let observerConfig = {
        root: null,
        rootMargin: "0px",
        threshold: [0]
      };

      this.observer = new IntersectionObserver(function(entries) {
        entries.forEach(function (entry) {
          if (entry.intersectionRatio > 0) {
            self.observer.unobserve(entry.target);
            let src = entry.target.getAttribute(self.settings.src);
            let srcset = entry.target.getAttribute(self.settings.srcset);
            if ("img" === entry.target.tagName.toLowerCase()) {
              if (src) {
                entry.target.src = src;
              }
              if (srcset) {
                entry.target.srcset = srcset;
              }
            } else {
              entry.target.style.backgroundImage = "url(" + src + ")";
            }
          }
        });
      }, observerConfig);

      this.images.forEach(function (image) {
        self.observer.observe(image);
      });
    },

    loadAndDestroy: function () {
      if (!this.settings) { return; }
      this.loadImages();
      this.destroy();
    },

    loadImages: function () {
      if (!this.settings) { return; }

      let self = this;
      this.images.forEach(function (image) {
        let src = image.getAttribute(self.settings.src);
        let srcset = image.getAttribute(self.settings.srcset);
        if ("img" === image.tagName.toLowerCase()) {
          if (src) {
            image.src = src;
          }
          if (srcset) {
            image.srcset = srcset;
          }
        } else {
          image.style.backgroundImage = "url('" + src + "')";
        }
      });
    },

    destroy: function () {
      if (!this.settings) { return; }
      this.observer.disconnect();
      this.settings = null;
    }
  };

  root.lazyload = function(images, options) {
    return new LazyLoad(images, options);
  };

  if (root.jQuery) {
    const $ = root.jQuery;
    $.fn.lazyload = function (options) {
      options = options || {};
      options.attribute = options.attribute || "data-src";
      new LazyLoad($.makeArray(this), options);
      return this;
    };
  }

  return LazyLoad;
});