Створення свого прогресивного завантажувача зображень

28

Від автора: ви могли бачити прогресивні зображення на Facebook і Medium. Замилені зображення низької якості замінюються на повнорозмірні версії, коли елемент потрапляє в поле зору.

Создание своего прогрессивного загрузчика изображений

Превью зображення крихітне. Можливо, сильно стиснутий JPEG, 20px в ширину. Файл може важити менше 300 байт, він з’являється миттєво, надаючи відчуття швидкого завантаження. Реальне зображення підвантажується по мірі необхідності з допомогою методу ледачою завантаження.

Прогресивні зображення це добре, але поточні рішення трохи складні. На щастя, можна створити своє на HTML5, CSS3 і JS. Код:

швидким та легким – всього 463 байта CSS і 1 007 байта JS (в минифицированном вигляді);

підтримувати адаптивні зображення для завантаження альтернативних версій під великі екрани або екрани з високою роздільною здатністю (ретина);

без залежностей – буде працювати з усіма фреймворками;

працювати у всіх сучасних браузерах (IE10+);

працювати в старих браузерах з техніки прогресивного поліпшення, або коли JS або завантаження зображень відмовили;

легким у використанні.

Наше демо та код на GitHub

Ось так виглядає наша техніка:

Завантажити код з GitHub

HTML

Для створення прогресивних зображень візьмемо простий HTML код:

image

Де:

full.jpg – повнорозмірне зображення, що зберігається в href, а

content-2/images/tiny.jpg – наше маленьке превью.

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

В обох зображень має бути однакове співвідношення сторін. Наприклад, якщо full.jpg має розміри 800 х 200, то співвідношення сторін становить 4:1. Превью content-2/images/tiny.jpg повинно бути 20 х 5. Не можна поставити ширину 30px, так як тоді потрібна буде неможлива висота 7.5 px.

Зверніть увагу на класи на засланні і превью зображенні. Це будуть наші хуки в JS.

Инлайнить або не инлайнить зображення

Превью зображення можна заинлайнить у вигляді data URI.

Створення свого прогресивного завантажувача зображень

Инлайновые зображення з’являються миттєво, вимагають менше HTTP запитів і зайвий раз не перебудовують сторінку. Однак:

инлайновое зображення набагато складніше вставити і змінити (може допомогти Gulp);

кодування base-64 менш ефективна і на 30% більше, ніж двійкові файли (хоча вага компенсується додатковими заголовками HTTP запитів);

инлайновые зображення не можна кешувати. Вони кешуються в HTML сторінці, але їх не можна використовувати на іншій сторінці без повторної посилки даних;

HTTP/2 прибирає необхідність у инлайновых зображеннях.

Будьте прагматичні: инлайновые зображення підходять, коли одна сторінка, або коли код невеликий, тобто не багато більше URL!

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: none 0;
}

Властивість height: auto обов’язково, так як IE10/11 може помилитися з висотою зображень.

Превью розмито зі значенням 2vw. Так розмиття здається однаковим незалежно від розмірів сторінки. Властивість overflow: hidden на контейнері надає зображенню чіткий край. Збільшена Превью на 1.05, щоб не було видно задній фон. Це означає, що для появи повного зображення можна використовувати приємний ефект зума.

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;}
}

Повне зображення поміщається поверх превью, після чого opacity збільшується з 0 до 1, а масштаб змінюється з 1.05 на 1 за одну секунду. За бажанням можна використовувати інші трансформації і/або фільтри.

JavaScript

Зараз ми попрактикуємось адаптивної техніки прогресивного поліпшення. JS код буде відразу перевіряти доступні браузерні API перед додаванням обробника події load на сторінку:

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

Подія load запускається після повного завантаження сторінки та всіх файлів. Нам не потрібно, щоб великі зображення почали завантажуватися до того, як будуть завантажені необхідні ресурси типу шрифтів, CSS, JS і превью зображень (що могло статися, якби ми використали події DOMContentLoaded, які запускаються по готовності DOM).

Потім ми отримуємо всі зображення в контейнері з класами 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 pT) {
loadFullImage(pItem[p]);
pItem[p].classList.remove(‘replace’);
}
else p++;
}
}

Коли контейнер у полі зору, його вузол передається у функцію loadFullImage(), а клас replace видаляється. Це миттєво видаляє вузол з pItem HTMLCollection, щоб контейнер не оброблявся повторно.

Функція 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’);
}
});
}
}

Цей код:

відключає подія click на контейнері;

для вставлення зображення на сторінку, що запускає плавне поява і зум;

чекає кінця анімації за допомогою обробника 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 до перемальовування.

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

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

Код зверху підтримує цю функцію – додайте атрибути data-srcset і data-sizes в контейнер з посиланнями, наприклад.

image

Після завантаження код повного зображення:

image

Сучасні браузери завантажують large.jpg коли ширина вьюпорта становить 800px і вище. Старі браузери і браузери з малою шириною вьюпорта отримають content-2/images/small.jpg. Більш докладно читайте в як створювати адаптивні зображення з допомогою srcset.

Зауваження

Я намагався не роздмухувати код, але ви можете його використовувати так чи поліпшити під будь-який проект. Що можна поліпшити:

Перевірку горизонтального скрола. Перевіряється тільки вертикальний скрол, тобто всі зображення в горизонтальній площині замінюються.

Динамічне додавання прогресивних зображень. Прогресивні зображення додаються сторінку через JS і замінюються тільки щодо подій scroll і resize.

Firefox продуктивність. Під час заміни великих зображень браузер може напружуватися, можна помітити миготіння.