Как создать собственный прогрессивный загрузчик изображений

Возможно, вы сталкивались с прогрессивными изображениями на Facebook и Medium. Размытое изображение с низким разрешением заменяется полновесной версией, когда при скроллинге страницы элемент входит в область просмотра.

Изображение для предварительного просмотра – это сжатое JPEG с шириной в 20 пикселей. Подобный файл может быть меньше 300 байт и отображаться мгновенно, чтобы создать впечатление быстрой загрузки. При необходимости реальное изображение загружается постепенно.

Прогрессивные изображения сложны в реализации. Эту технику можно воссоздать с помощью HTML5, CSS3 и JavaScript. Наш код будет:

  1. быстрым и компактным — всего 463 байта CSS и 1007 байтов JavaScript (минимизированные);
  2. поддерживать адаптивные изображения с загрузкой альтернативных версий для экранов с высоким разрешением;
  3. работать с любым фреймворком;
  4. работать во всех современных браузерах (IE10+);
  5. простым в использовании.
Содержание

Демокод и проект в GitHub

Вот как будет выглядеть наша техника:

Посмотреть демо.

Загрузить код с GitHub

HTML-код

Начнем с базового HTML-кода:

<a href="full.jpg" class="progressive replace">
<img src="tiny.jpg" class="preview" alt="image" />
</a>

где:

  • full.jpg — большое полноразмерное изображение.
  • tiny.jpg — крошечное изображение для предварительного просмотра.

Версия реализации без JavaScript – это подойдет для устаревших браузеров. Пользователь сможет просмотреть полноценно изображение, кликнув по превью.

Оба изображения должны иметь одинаковое соотношение сторон. Например, full.jpg имеет размеры 800 на 200 пикселей и соотношение сторон 4: 1. Тогда tiny.jpg может иметь размеры 20 на 5 пикселей. Но при этом нельзя использовать ширину 30 пикселей, для которой потребуется высота 7.5 пикселей.

Обратите внимание на классы в ссылке и изображении превью. Они будут использоваться как хуки в JavaScript- коде.

Встраивать или не встраивать изображения

Изображение превью может быть задано через атрибут src. Например:

<img src="data:image/jpeg;base64,/9j/4AAQSkZJ..."  class="preview" />

Встроенные изображения отображаются мгновенно. Но у них есть свои недостатки:

  1. требуется больше усилий, чтобы добавить или изменить встроенное изображение;
  2. кодирование base-64 менее эффективно, а исходный код обычно на 30% объемнее, чем двоичные данные (хотя это компенсируется дополнительными заголовками HTTP-запросов);
  3. встроенные изображения нельзя кэшировать. Они будут кэшироваться на HTML-странице, но их нельзя использовать на другой странице без повторной отправки данных.
  4. HTTP / 2 снижает необходимость использования встроенных изображений.

CSS

Начнем с определения стилей контейнера ссылок:

a.progressive {
  position: relative;
  display: block;
  overflow: hidden;
  outline: none;
}

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

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

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

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

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

a.progressive:not(.replace) {
  cursor: default;
}

Маленькие и большие изображения в контейнере имеют размер, соответствующий ширине контейнера:

a.progressive img {
  display: block;
  width: 100%;
  max-width: none;
  height: auto;
  border: 0 none;
}

Использование height: auto. Без этой строки IE10 / 11 не смогут рассчитать высоту изображения.

Рисунок превью размыт с использованием длины в 2vw. Это гарантирует, что размытия будет выглядеть одинаково независимо от размеров страницы. Свойство overflow: hidden, примененное к контейнеру обеспечивает, четкий край изображения. Также мы задаем для него увеличение 1.05, чтобы цвета не переходили на фон страницы через внешний край размытого изображения. Это позволяет использовать эффект масштабирования, чтобы отобразить полное изображение.

a a.progressive img.preview {
  filter: blur(2vw);
  transform: scale(1.05);
}

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

a.progressive img.reveal {
  position: absolute;
  left: 0;
  top: 0;
  will-change: transform, opacity;
  animation: reveal 1s ease-out;
}

@keyframes reveal {
  0% {transform: scale(1.05); opacity: 0;}
  100% {transform: scale(1); opacity: 1;}
}

Полновесная версия рисунка располагается над превью. Затем его непрозрачность увеличивается с 0 до 1, а масштаб с 1,05 до 1 в течение одной секунды. Вы можете применить другие преобразования или эффекты фильтров.

JavaScript

Сначала JavaScript проверяет, доступны ли необходимые API-интерфейсы браузера, прежде чем добавлять на страницу прослушиватель события load:

// progressive-image.js
if (window.addEventListener && window.requestAnimationFrame && document.getElementsByClassName) window.addEventListener('load', function() {

Событие load наступает, когда веб-страница и используемые ею ресурсы загружены. Не нужно, чтобы большие изображения загружались до загрузки шрифтов, CSS, JavaScript и превью.

Затем получаем все элементы контейнера изображений с именами классов progressive и replace:

var pItem = document.getElementsByClassName('progressive replace'), timer;

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

Затем мы вызываем функцию inView(). Она проверяет, находится ли каждый контейнер в области просмотра, сравнивая его положение getBoundingClientRect с вертикальной полосой прокрутки window.pageYOffset:

// изображение видно?
function inView() {
  var wT = window.pageYOffset, wB = wT + window.innerHeight, cRect, pT, pB, p = 0;
  while (p < pItem.length) {

    cRect = pItem[p].getBoundingClientRect();
    pT = wT + cRect.top;
    pB = pT + cRect.height;

    if (wT < pB && wB > pT) {
      loadFullImage(pItem[p]);
      pItem[p].classList.remove('replace');
    }
    else p++;
  }
}

Когда контейнер находится в области просмотра, его узел передается функции loadFullImage(), и класс replace удаляется. Это мгновенно удаляет узел из pItemHTMLCollection, поэтому контейнер не будет повторно обрабатываться.

Функция loadFullImage() создает новый HTML- объект Image(), копирует href контейнера в атрибут src и применяет класс reveal:

// заменяем полным изображением
function loadFullImage(item) {
  if (!item || !item.href) return;

  // загружаем изображение
  var img = new Image();
  if (item.dataset) {
    img.srcset = item.dataset.srcset || '';
    img.sizes = item.dataset.sizes || '';
  }
  img.src = item.href;
  img.className = 'reveal';
  if (img.complete) addImg();
  else img.onload = addImg;

После загрузки изображения вызывается функция addImg:

// заменяем изображение
  function addImg() {
    // отключаем клик
    item.addEventListener('click', function(e) { e.preventDefault(); }, false);

    // добавляем полное изображение
    item.appendChild(img).addEventListener('animationend', function(e) {
      // удаляем изображение превью
      var pImg = item.querySelector && item.querySelector('img.preview');
      
      if (pImg) {
        e.target.alt = pImg.alt || '';
        item.removeChild(pImg);
        e.target.classList.remove('reveal');
      }
    });
  }
}

Приведенный выше код:

  1. отключает для контейнера событие click;
  2. добавляет изображение на страницу, запуская при этом анимацию затухания / увеличения;
  3. ждет завершения анимации с помощью прослушивателя animationend, затем копирует тег alt, удаляет узел изображения превью и класс reveal из полновесной версии. Что увеличивает производительность и предотвращает возникновение проблем с размером окна в браузере Edge.

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

inView();

Мы также вызываем эту функцию при прокрутке страницы или изменении размеров окна браузера. Некоторые устаревшие браузеры (в первую очередь IE) могут быстро реагировать на эти события. Поэтому мы ограничим возможность повторного вызова функции в течение 300 миллисекунд:

window.addEventListener('scroll', scroller, false);
window.addEventListener('resize', scroller, false);

function scroller(e) {
  timer = timer || setTimeout(function() {
    timer = null;
    requestAnimationFrame(inView);
  }, 300);
}

Обратите внимание на вызов requestAnimationFrame, который выполняется inView до следующей перерисовки.

Адаптивные изображения

Атрибуты srcset и sizes, доступные в HTML5, определяют несколько различных размеров и разрешений для изображений. Браузер выбирает наиболее подходящую для устройства версию.

Приведенный выше код поддерживает эту функцию. Пример добавления атрибутов data-srcset и data-sizes для контейнера ссылок приведен ниже.

<a href="small.jpg"
  data-srcset="small.jpg 800w, large.jpg 1200w"
  data-sizes="100vw"
  class="progressive replace">
  <img src="preview.jpg" class="preview" alt="image" />
</a>

После загрузки код полновесного изображения будет выглядеть следующим образом:

<img src="small.jpg"
    srcset="small.jpg 800w, large.jpg 1200w"
    sizes="100vw"
    alt="image" />

Современные браузеры будут загружать large.jpg, когда ширина области просмотра составляет 800 пикселей или больше. Устаревшие браузеры будут загружать изображение small.jpg.

Примечания по использованию

Возможные усовершенствования кода:

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

Данная публикация представляет собой перевод статьи «How to Build Your Own Progressive Image Loader» , подготовленной дружной командой проекта Интернет-технологии.ру