Привет, друзья!
На днях мне посчастливилось заниматься решением 2 несложных, но довольно интересных задач на чистом JavaScript
(из-за React
чуть не забыл, как это делается). В процессе решения этих задач никто не пострадал, напротив, все остались довольны. Поэтому я решил поделиться результатами с сообществом.
Обратите внимание: данная статья рассчитана, преимущественно, на начинающих разработчиков и вряд ли покажется интересной опытным, хотя, смею надеяться, что и последние не пожалеют потраченного времени, если прочитают ее до конца.
Введение
Итак, задачи были следующими:
- Реализовать навигацию по разделам сайта с визуальным переключением индикатора текущего местонахождения пользователя и возможностью переходить к определенному разделу по клику. Сама навигация согласно макету должна была выглядеть так:
- Реализовать "ленивую" (отложенную, lazy) загрузку медиаресурсов (изображений, аудио и видео), поскольку те же изображения даже после сжатия с помощью
gulp-imagemin
весили (и весят, потому что они никуда не делись) неприличные 50 Мб и загружались при запуске приложения (больше они себя так не ведут).
Как я уже сказал, обе задачи надо было решить на ванильном JS
. Почему? Потому что проект был, что называется, legacy — Python
в лице Wagtail
и JS
в лице JQuery
.
На первый взгляд, может показаться, что названные задачи являются совершенно разными и между ними не может быть ничего общего. “Что может их объединять?” — спросите вы.
Ответ: технология, которую можно использовать для их решения. Вы не ослышались, обе задачи можно решить с помощью одного инструмента и при этом его даже не придется устанавливать. Для решения задач нам не потребуются ни сторонние библиотеки, ни вспомогательные утилиты — интерфейс, о котором идет речь, предоставляется самим браузером.
Как вы уже могли догадаться по названию статьи, я говорю об Intersection Observer API
(далее — IOA
). Данный интерфейс поддерживается всеми современными браузерами.
Если кратко, то IOA
позволяет асинхронно регистрировать момент пересечения целевого элемента с его предком, если таковой определен в настройках, или с областью видимости документа (viewport
).
Экземпляр IOA
создается так:
const observer = new IntersectionObserver(callback, options)
Конструктор IntersectionObserver
принимает 2 параметра: функцию обратного вызова, запускаемую в момент пересечения — callback
, и объект с настройками — options
.
Объект с настройками может содержать 3 поля:
-
root
— родительский элемент, за пересечением с которым (целевым элементом) наблюдает наблюдатель (извините за тавтологию). Еслиroot
не определен или имеет значениеnull
, предком целевого элемента считаетсяviewport
-
rootMargin
— отступы вокругroot
. Как и для CSS-свойстваmargin
, значения дляrooMargin
могут задаваться в пикселях, процентах, em, rem и т.д., например,20px 10px
. По умолчанию данная настройка имеет значение0
-
threshold
— процент видимости целевого элемента от0
до1
. Значение данной настройки может задаваться в виде числа или массива чисел. По умолчанию также имеет значение0
Наблюдение за целевым элементом устанавливается посредством вызова метода observe
:
observer.observe(target)
Существует также несколько других методов, но мы будет использовать только observe
.
При достижении целевым элементом порогового значения (threshold
) вызывается callback
, который получает список объектов entries
и самого наблюдателя. Каждый объект entry
имеет несколько свойств (target
, time
, intersectionRatio
и др.). Единственным свойством, которое нас интересует, является isIntersection
— логическое значение, выступающее индикатором нахождения целевого элемента в зоне пересечения.
Кстати, процент пересечения также можно определять с помощью свойства intersectionRatio
.
Пожалуй, это все, что нам нужно знать об IOA
для того, чтобы успешно решить наши задачи. Приступаем.
Начнем с навигации.
Реализация навигации по разделам сайта
Давайте рассуждать.
Элементы меню (ссылки) соответствуют разделам страницы (заголовкам разделов, если быть точнее). Каждому заголовку соответствует определенная ссылка. Мы это знаем, но этого не знает JS
. Поэтому нам нужно как-то сообщить ему об этих отношениях один к одному
. Существует несколько способов это сделать, но самым простым является использование атрибута data-*
. Назовем его data-section
. Пускай для простоты значениями этих атрибутов будут номера разделов (1, 2, 3 и т.д.).
Также нам нужен какой-то способ визуального отображения текущего раздела, т.е. раздела, просматриваемого пользователем. Самым простым способом это сделать является добавление к такому разделу специального CSS-класса. Назовем этот класс active
.
Вот как будет выглядеть один элемент навигации и соответствующий ему раздел:
<!-- элемент навигации -->
<div class="step active" data-section="1">
<span class="text">Section 1</span>
<div class="dot"></div>
</div>
<!-- раздел страницы, соответствующий этому элементу -->
<section data-section="1">
<h2>Section 1</h2>
<p>Много lorem</p>
</section>
Таких элементов и разделов у нас будет 5 штук.
<div id="page">
<nav aria-label="section navigation">
<div class="step active" data-section="1">
<span class="text">Section 1</span>
<div class="dot"></div>
</div>
<div class="line"></div>
<div class="step" data-section="2">
<span class="text">Section 2</span>
<div class="dot"></div>
</div>
<div class="line"></div>
<div class="step" data-section="3">
<span class="text">Section 3</span>
<div class="dot"></div>
</div>
<div class="line"></div>
<div class="step" data-section="4">
<span class="text">Section 4</span>
<div class="dot"></div>
</div>
<div class="line"></div>
<div class="step" data-section="5">
<span class="text">Section 5</span>
<div class="dot"></div>
</div>
</nav>
<main>
<section data-section="1">
<h2>Section 1</h2>
<p>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Sit
deserunt ea reiciendis sint ex recusandae repudiandae, ratione non
vero! Aliquam sed quis inventore aperiam quidem, velit illo
voluptates amet architecto aspernatur, et neque ipsam veritatis
itaque eius est vero illum officiis totam, a hic. Ex est quae vitae
itaque suscipit dolorem corporis assumenda exercitationem quos iure,
deserunt repellendus provident possimus, architecto iste mollitia
nemo dicta dolores ipsa labore deleniti doloremque? Sit omnis minima
quis voluptate sapiente rerum fugit totam ut illo reprehenderit
earum ratione temporibus sequi odio consectetur perspiciatis, soluta
quidem dicta aliquid. Totam, animi placeat quisquam ducimus
adipisci, velit ipsam alias inventore earum sit eveniet rem hic
labore repellendus, aut quam reprehenderit esse temporibus quas quo
laudantium fuga! Quidem tempore illum atque sit optio nisi, eius
reiciendis perferendis similique, voluptatum maxime qui ipsam
deleniti vitae quia vero ad consequatur aspernatur voluptatibus quo
facere? Consequuntur, non ipsa? Assumenda repellat impedit dolores
eius dolorem debitis eaque aspernatur deleniti optio atque, est iure
excepturi ullam odio, natus quos sit! Nostrum aspernatur quos esse
quisquam! Aut exercitationem aspernatur expedita quam ullam quas quo
dignissimos. Animi ex incidunt architecto, quaerat vel qui illum
ipsa dolores nesciunt sapiente suscipit odio facere corrupti.
Itaque, provident quis?
</p>
</section>
<section data-section="2">
<h2>Section 2</h2>
<p>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Sit
deserunt ea reiciendis sint ex recusandae repudiandae, ratione non
vero! Aliquam sed quis inventore aperiam quidem, velit illo
voluptates amet architecto aspernatur, et neque ipsam veritatis
itaque eius est vero illum officiis totam, a hic. Ex est quae vitae
itaque suscipit dolorem corporis assumenda exercitationem quos iure,
deserunt repellendus provident possimus, architecto iste mollitia
nemo dicta dolores ipsa labore deleniti doloremque? Sit omnis minima
quis voluptate sapiente rerum fugit totam ut illo reprehenderit
earum ratione temporibus sequi odio consectetur perspiciatis, soluta
quidem dicta aliquid. Totam, animi placeat quisquam ducimus
adipisci, velit ipsam alias inventore earum sit eveniet rem hic
labore repellendus, aut quam reprehenderit esse temporibus quas quo
laudantium fuga! Quidem tempore illum atque sit optio nisi, eius
reiciendis perferendis similique, voluptatum maxime qui ipsam
deleniti vitae quia vero ad consequatur aspernatur voluptatibus quo
facere? Consequuntur, non ipsa? Assumenda repellat impedit dolores
eius dolorem debitis eaque aspernatur deleniti optio atque, est iure
excepturi ullam odio, natus quos sit! Nostrum aspernatur quos esse
quisquam! Aut exercitationem aspernatur expedita quam ullam quas quo
dignissimos. Animi ex incidunt architecto, quaerat vel qui illum
ipsa dolores nesciunt sapiente suscipit odio facere corrupti.
Itaque, provident quis?
</p>
</section>
<section data-section="3">
<h2>Section 3</h2>
<p>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Sit
deserunt ea reiciendis sint ex recusandae repudiandae, ratione non
vero! Aliquam sed quis inventore aperiam quidem, velit illo
voluptates amet architecto aspernatur, et neque ipsam veritatis
itaque eius est vero illum officiis totam, a hic. Ex est quae vitae
itaque suscipit dolorem corporis assumenda exercitationem quos iure,
deserunt repellendus provident possimus, architecto iste mollitia
nemo dicta dolores ipsa labore deleniti doloremque? Sit omnis minima
quis voluptate sapiente rerum fugit totam ut illo reprehenderit
earum ratione temporibus sequi odio consectetur perspiciatis, soluta
quidem dicta aliquid. Totam, animi placeat quisquam ducimus
adipisci, velit ipsam alias inventore earum sit eveniet rem hic
labore repellendus, aut quam reprehenderit esse temporibus quas quo
laudantium fuga! Quidem tempore illum atque sit optio nisi, eius
reiciendis perferendis similique, voluptatum maxime qui ipsam
deleniti vitae quia vero ad consequatur aspernatur voluptatibus quo
facere? Consequuntur, non ipsa? Assumenda repellat impedit dolores
eius dolorem debitis eaque aspernatur deleniti optio atque, est iure
excepturi ullam odio, natus quos sit! Nostrum aspernatur quos esse
quisquam! Aut exercitationem aspernatur expedita quam ullam quas quo
dignissimos. Animi ex incidunt architecto, quaerat vel qui illum
ipsa dolores nesciunt sapiente suscipit odio facere corrupti.
Itaque, provident quis?
</p>
</section>
<section data-section="4">
<h2>Section 4</h2>
<p>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Sit
deserunt ea reiciendis sint ex recusandae repudiandae, ratione non
vero! Aliquam sed quis inventore aperiam quidem, velit illo
voluptates amet architecto aspernatur, et neque ipsam veritatis
itaque eius est vero illum officiis totam, a hic. Ex est quae vitae
itaque suscipit dolorem corporis assumenda exercitationem quos iure,
deserunt repellendus provident possimus, architecto iste mollitia
nemo dicta dolores ipsa labore deleniti doloremque? Sit omnis minima
quis voluptate sapiente rerum fugit totam ut illo reprehenderit
earum ratione temporibus sequi odio consectetur perspiciatis, soluta
quidem dicta aliquid. Totam, animi placeat quisquam ducimus
adipisci, velit ipsam alias inventore earum sit eveniet rem hic
labore repellendus, aut quam reprehenderit esse temporibus quas quo
laudantium fuga! Quidem tempore illum atque sit optio nisi, eius
reiciendis perferendis similique, voluptatum maxime qui ipsam
deleniti vitae quia vero ad consequatur aspernatur voluptatibus quo
facere? Consequuntur, non ipsa? Assumenda repellat impedit dolores
eius dolorem debitis eaque aspernatur deleniti optio atque, est iure
excepturi ullam odio, natus quos sit! Nostrum aspernatur quos esse
quisquam! Aut exercitationem aspernatur expedita quam ullam quas quo
dignissimos. Animi ex incidunt architecto, quaerat vel qui illum
ipsa dolores nesciunt sapiente suscipit odio facere corrupti.
Itaque, provident quis?
</p>
</section>
<section data-section="5">
<h2>Section 5</h2>
<p>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Sit
deserunt ea reiciendis sint ex recusandae repudiandae, ratione non
vero! Aliquam sed quis inventore aperiam quidem, velit illo
voluptates amet architecto aspernatur, et neque ipsam veritatis
itaque eius est vero illum officiis totam, a hic. Ex est quae vitae
itaque suscipit dolorem corporis assumenda exercitationem quos iure,
deserunt repellendus provident possimus, architecto iste mollitia
nemo dicta dolores ipsa labore deleniti doloremque? Sit omnis minima
quis voluptate sapiente rerum fugit totam ut illo reprehenderit
earum ratione temporibus sequi odio consectetur perspiciatis, soluta
quidem dicta aliquid. Totam, animi placeat quisquam ducimus
adipisci, velit ipsam alias inventore earum sit eveniet rem hic
labore repellendus, aut quam reprehenderit esse temporibus quas quo
laudantium fuga! Quidem tempore illum atque sit optio nisi, eius
reiciendis perferendis similique, voluptatum maxime qui ipsam
deleniti vitae quia vero ad consequatur aspernatur voluptatibus quo
facere? Consequuntur, non ipsa? Assumenda repellat impedit dolores
eius dolorem debitis eaque aspernatur deleniti optio atque, est iure
excepturi ullam odio, natus quos sit! Nostrum aspernatur quos esse
quisquam! Aut exercitationem aspernatur expedita quam ullam quas quo
dignissimos. Animi ex incidunt architecto, quaerat vel qui illum
ipsa dolores nesciunt sapiente suscipit odio facere corrupti.
Itaque, provident quis?
</p>
</section>
</main>
</div>
Стили вполне себе обычные, так что на них я останавливаться не буду.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Montserrat', sans-serif;
font-size: 1rem;
}
:root {
--main-light: #f0f0f0;
--main-dark: #3c3c3c;
--main-blue: #c5dfee;
--active-blue: #53acff;
}
body {
background: var(--main-light);
color: var(--main-dark);
}
#page {
max-width: 95%;
}
nav {
display: flex;
flex-direction: column;
position: fixed;
top: 2rem;
right: 2rem;
width: 20%;
}
main {
width: 80%;
padding: 1rem;
text-indent: 1.25rem;
}
h2 {
font-size: 2rem;
margin: 1rem 0;
}
p {
font-size: 1.1rem;
}
.dot {
position: absolute;
width: 10px;
height: 10px;
right: 0;
top: 50%;
transform: translateY(-50%);
border-radius: 50%;
transition: 0.4s ease;
}
.dot:after {
content: '';
position: absolute;
top: 50%;
right: 50%;
transform: translate(50%, -50%);
width: 10px;
height: 10px;
background-color: var(--main-blue);
border-radius: 50%;
transition: 0.4s ease;
}
.active .dot {
width: 26px;
height: 26px;
border: 1px solid var(--active-blue);
right: -8px;
}
.active .dot:after {
width: 16px;
height: 16px;
background-color: var(--active-blue);
}
.step {
position: relative;
padding-right: 22px;
font-size: 16px;
display: flex;
align-items: center;
margin-left: auto;
transition: all 0.4s ease;
cursor: pointer;
user-select: none;
}
.active .text {
color: var(--active-blue);
font-size: 20px;
margin: 10px 0 10px auto;
}
.line {
height: 44px;
width: 1px;
background-color: var(--main-blue);
margin: -10px 4px -10px auto;
transition: all 0.4s ease;
}
Как вы думаете, сколько строк JS-кода занимает решение данной задачи? Не забывайте о том, что кроме выделения элемента навигации, соответствующего текущему разделу, с помощью стилей, нам также необходимо реализовать переключение между разделами по клику (при этом визуализация должна отрабатывать корректно). Так сколько же? 100? 50?
Ответ: 30 строк! И это без экономии на пробелах и отступах.
Завернем весь наш код в функцию. Назовем ее createSectionNav
. Пусть функция принимает корневой элемент — root
:
// определяем функцию
const createSectionNav = (root) => {
// ...
}
// вызываем ее с корневым элементом
createSectionNav(document.querySelector('#page'))
Получаем ссылки на nav
, main
и текущий (активный) элемент навигации (далее — шаг):
const nav = root.querySelector('nav')
const main = root.querySelector('main')
let currentActiveStep = nav.querySelector('.active')
Сначала реализуем переключение между разделами по клику. Для прокрутки к определенному элементу мы воспользуемся глобальным методом scrollIntoView
. А находить нужный элемент будем с помощью атрибута data-section
:
nav.addEventListener('click', ({ target }) => {
// извлекаем значение атрибута `data-section`
const { section } = target.closest('.step').dataset
if (!section) return
// находим соответствующий элемент и выполняем к нему прокрутку
main.querySelector(`[data-section='${section}']`).scrollIntoView({
// плавно
behavior: 'smooth'
})
})
В принципе, если на странице много разделов, для того, чтобы каждый раз не искать нужный раздел с помощью querySelector
, можно предварительно составить карту разделов:
const sectionMap = [...main.querySelectorAll('[data-section]')].reduce(
(obj, el) => {
obj[el.dataset.section] = el
return obj
},
{}
)
И обращаться к соответствующему разделу по ключу:
sectionMap[section].scrollIntoView({
behavior: 'smooth'
})
Это все, что требуется для переключения между разделами. “А как же визуальная составляющая?” — спросите вы. О, это самое интересное. Визуальная составляющая будет полностью инкапсулирована в наблюдателе.
Перебираем разделы:
main.querySelectorAll("section").forEach((s) => {
// ...
}
Из настроек нам требуется только процент пересечения. Установим его в значение 0.3
(30%):
const options = { threshold: 0.3 }
Функция обратного вызова делает следующее:
- принимает список объектов
entries
- извлекает первый
entry
- проверяет, находится ли он зоне пересечения
- если находится
- извлекает номер раздела из его
data-section
- удаляет класс
active
у текущего активного элемента - определяет новый активный элемент
- добавляет к нему класс
active
- и присваивает новое значение переменной
currentActiveStep
- извлекает номер раздела из его
const callback = (entries) => {
if (entries[0].isIntersecting) {
const { section } = s.dataset
if (!section) return
currentActiveStep.classList.remove("active")
const newActiveStep = nav.querySelector(
`[data-section='${section}']`
)
newActiveStep.classList.add("active")
currentActiveStep = newActiveStep
}
}
Создаем наблюдателя:
const observer = new IntersectionObserver(callback, options)
И начинаем следить за разделом:
observer.observe(s)
Если все это объединить, то получится следующее:
main.querySelectorAll("section").forEach((s) => {
new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
const { section } = s.dataset
if (!section) return
currentActiveStep.classList.remove("active")
const newActiveStep = nav.querySelector(
`[data-section='${section}']`
)
newActiveStep.classList.add("active")
currentActiveStep = newActiveStep
}
},
{
threshold: 0.3
}
).observe(s)
})
Таким образом, визуальная составляющая полностью инкапсулирована в наблюдателе: колбэк запускается как при "ручной" прокрутке страницы, так и при клике по шагу — в этом случае выполняется автоматическая прокрутка, также приводящая к регистрации пересечения.
Поиграть с этим кодом можно здесь:
Теперь поговорим о ленивой загрузке.
Реализация ленивой загрузки медиаресурсов
“В чем проблема обычной загрузки медиаресурсов?” — спросите вы. Когда таких ресурсов немного, никакой проблемы нет. Но когда их много и когда компоненты приложения загружаются разом (в приложении не реализован code splitting
— разделение кода на уровне компонентов), все ресурсы, используемые этими компонентами, загружаются вместе с ними. Большое количество ресурсов означает "много байт" полезной нагрузки, а много байт полезной нагрузки означает снижение производительности. Следовательно, чем больше весят загружаемые ресурсы, тем хуже производительность приложения.
Как только браузер при разборе разметки встречает атрибут src
(например, <img src="https://example.com/some_img.png" alt="" role="presentation" />
), он тут же загружает ресурс из указанного в атрибуте источника. Все ресурсы, загружаемые браузером при запуске приложения, можно найти в разделе Network
(Сеть
) инструментов разработчика (ресурсы можно фильтровать с помощью вкладок JS
, CSS
, Img
и др.). Вот, например, Chrome
загрузил аватар для моего Google-аккаунта:
Вы можете спросить: "Неужели браузеры не предоставляют какой-либо нативной технологии для реализации ленивой загрузки медиа?" В действительности, кое-что они для этого предоставляют, но… Конечно, есть но, иначе не было бы этой статьи.
Существует атрибут loading
. Если добавить его к изображению или фрейму (iframe
) со значением lazy
, то они будут загружаться лениво. И все бы ничего, вот только данный атрибут не поддерживается Safari
, а там, где поддерживается, срабатывает не всегда корректно, поэтому для продакшна он не подходит.
Загрузкой аудио и видео можно управлять с помощью атрибута preload
. Если установить его в значение none
, то медиа будет загружаться только после нажатия пользователем кнопки Play
. Данный атрибут поддерживается всеми современными браузерами. Но его возможности являются довольно ограниченными.
Все-таки хотелось бы иметь какой-то общий подход к реализации ленивой загрузки медиа. Кроме того, основной вес большинства страниц составляют изображения.
Для того, чтобы избежать загрузки ресурсов при запуске приложения, необходимо избавиться от всех атрибутов src
, но при этом иметь возможность загружать ресурсы… при пересечении соответствующих элементов с viewport
! Здесь нам снова пригодится атрибут data-*
. Заменим src
на data-src
:
<img data-src="https://images.unsplash.com/photo-1629290211578-213dc1f621e1" alt="" role="presentation" />
Разметка этого приложения будет похожа на разметку предыдущего, за исключением следующего:
- нам не нужна панель навигации
- нам хватит 3 разделов
- между разделами будут находиться медиаресурсы: одно изображение, одно аудио и одно видео.
<main>
<section>
<h2>Section 1</h2>
<p>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Adipisci ut
corrupti deleniti reiciendis, consectetur tenetur libero voluptates,
repudiandae saepe, sit fugit? Eos expedita commodi maiores ipsum quo
cum, vel esse enim doloribus repudiandae beatae sint corporis amet
eaque aliquid, nostrum est. Labore rem incidunt perspiciatis qui
aliquid adipisci quasi, sapiente veritatis accusamus earum id animi
voluptas tempore, totam hic provident aut perferendis! Voluptate natus
enim quae necessitatibus provident at dolores? Soluta cum iusto facere
officia sunt dolores placeat, qui autem, esse nostrum quaerat, enim
quisquam. Error totam ducimus quos. Rem dolor dolorem nam in assumenda
ipsa nisi fuga voluptatibus officiis saepe, asperiores atque quasi
error dolore corrupti, quidem exercitationem? Consectetur a quis iusto
distinctio. Quis architecto quos doloremque mollitia ut nulla et
libero expedita? Suscipit, sint minus doloremque neque mollitia
aliquam ex accusantium deleniti ipsum laborum voluptas. Modi, incidunt
porro dolorum nobis sapiente, nulla rem vel dolores quisquam voluptas
pariatur nostrum ipsum obcaecati explicabo, assumenda minima
voluptatibus mollitia cum fugit eius temporibus officia voluptates?
Animi, explicabo officiis impedit vero quis facilis quia optio
sapiente exercitationem! Eum ad fugit velit pariatur sed cum
temporibus dolore magni illum quas repellendus quae hic vitae
deserunt, repellat nostrum quaerat ipsam, officia minima
necessitatibus reprehenderit. Alias repellat voluptatibus soluta vel
cupiditate fuga pariatur amet. Aperiam, at cum officia, eaque adipisci
id laudantium in repellendus doloribus iste exercitationem accusantium
facere ipsa omnis optio, ea neque nihil incidunt? Necessitatibus eum
laudantium deleniti sint vero vel possimus architecto, eaque quam
voluptate? Odio aliquam officiis sunt, harum fuga vitae neque,
obcaecati repellendus beatae et amet rerum facilis at ipsum unde
sapiente. Maxime rem quaerat iusto non dignissimos, eius tempora
ducimus, necessitatibus impedit eum, corporis voluptatibus alias sit
cupiditate. Error numquam, voluptatem quas et modi, eos, excepturi
animi explicabo aut cupiditate minus! Quos tempora, molestiae atque ea
adipisci porro optio.
</p>
</section>
<!-- https://caniuse.com/loading-lazy-attr -->
<div>
<!-- <img
data-src="https://images.unsplash.com/photo-1629290211578-213dc1f621e1"
alt=""
role="presentation"
loading="lazy"
/> -->
<img data-src="https://images.unsplash.com/photo-1629290211578-213dc1f621e1" alt="" role="presentation" />
</div>
<section>
<h2>Section 2</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. At, odit
nisi saepe, fuga cupiditate reprehenderit sequi ea dolore accusamus
incidunt quidem impedit? Adipisci magnam eveniet dolorum numquam ex
nam culpa exercitationem earum, sint tenetur doloribus dolore
recusandae unde at soluta, non libero accusamus hic esse doloremque
quibusdam? Architecto quo, aliquam ex id ullam eveniet laboriosam
tempora sapiente quas delectus recusandae alias, quidem accusantium
numquam blanditiis sit aspernatur, explicabo perspiciatis consectetur?
Provident, magnam possimus tenetur mollitia impedit atque nesciunt
voluptas aspernatur porro expedita culpa accusamus esse, non tempora
suscipit! Quos facilis, accusantium voluptate corporis dolorum
suscipit inventore minima cumque modi a deserunt reprehenderit nisi
officia molestias voluptatibus rerum saepe laudantium aperiam, non
eligendi ratione voluptas ducimus nemo! Mollitia qui quibusdam maiores
commodi libero perferendis iure, tenetur eaque? Ea, similique officia
illum dolor iusto enim! Ratione, voluptate! Possimus rerum ratione est
repellendus, asperiores quos quaerat ex at placeat ipsam assumenda
tempora, corrupti sint laborum consectetur non dolorum totam sed,
architecto quam. Nesciunt consequuntur rem necessitatibus ut beatae,
recusandae culpa sint maiores numquam nam cumque delectus eos facere
ab iste at, asperiores provident. Quo sunt nulla delectus officiis
enim sapiente illum, porro, eaque ut ipsum nostrum qui? Omnis est
consequatur pariatur labore odit maxime ea libero sit, veritatis
assumenda ducimus exercitationem laborum suscipit excepturi quis saepe
eaque illo aspernatur officiis inventore, commodi similique cupiditate
quos veniam. Natus distinctio voluptatibus accusamus officia corporis
cum esse magni a, repudiandae sed debitis sequi delectus, ex
accusantium. Illum voluptas eos aut aliquid facere unde. Ad
exercitationem quibusdam, temporibus dolorem nihil commodi soluta sunt
facilis aspernatur aperiam. Eaque expedita mollitia odio, in facilis
fuga aperiam neque incidunt quaerat id, exercitationem eum voluptas
nostrum quo soluta a dolores veritatis impedit dolor nihil placeat
itaque officiis! Quos consequuntur perspiciatis unde tenetur nemo quo
cumque, voluptas qui ex alias, nesciunt blanditiis.
</p>
</section>
<!-- https://caniuse.com/mdn-html_elements_audio_preload -->
<div>
<!-- <audio
src="https://www.bensound.com/bensound-music/bensound-happyrock.mp3"
controls
preload="none"
></audio> -->
<audio data-src="https://www.bensound.com/bensound-music/bensound-happyrock.mp3" controls></audio>
</div>
<section>
<h2>Section 3</h2>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Libero quasi
architecto fuga voluptatum deleniti exercitationem, mollitia
similique, facere rem ad dolorum tenetur eaque a voluptas nobis
repellendus? Maxime est nemo placeat deleniti cumque modi ipsam hic
porro rem unde quae, dignissimos minima quam nesciunt temporibus
asperiores repudiandae voluptatibus in iure facere. Vitae itaque
numquam tempora! Eius accusantium excepturi ea omnis earum, minus fuga
sint totam iusto voluptatum, natus sit quidem! Deleniti nulla quia at
qui omnis accusamus nobis delectus ex, assumenda odio illum nemo ipsa
placeat quam vero, necessitatibus alias? Facilis ut explicabo iusto
atque corrupti similique nam temporibus totam neque molestiae. Saepe
esse veniam, iure deleniti modi vel voluptas rerum, assumenda fugit,
architecto ratione neque aliquid adipisci beatae vero tempora mollitia
ducimus perspiciatis maxime. Velit amet cupiditate harum nostrum modi
repellat unde error minus maxime inventore. Corrupti distinctio modi
ratione inventore maxime fugit voluptate dolore, itaque, cum fuga quia
pariatur deleniti sit possimus! Voluptas repudiandae alias earum quis
ipsa eveniet optio aliquid maxime placeat quae laboriosam eaque vitae
dicta impedit, adipisci ullam quas dolore architecto minima quibusdam
expedita accusantium eos explicabo? Iusto inventore dolorum ipsa
eligendi ea, totam nam sed et impedit similique quidem, esse non.
Dolore amet molestias commodi reiciendis nesciunt, nihil enim tempora
magni tenetur illo ex, consectetur labore, repellendus iusto at
laborum quae harum corporis corrupti? Rerum dolorum, velit laboriosam
eius vero dolorem voluptas laudantium, accusantium aspernatur esse
necessitatibus deleniti tempora et possimus, ut obcaecati nulla.
Ducimus sed amet magnam odio officiis fugit cum voluptatum expedita
placeat asperiores voluptas maxime, consequuntur ratione id provident?
Perspiciatis explicabo animi, ut incidunt reiciendis ad praesentium
asperiores atque distinctio quae. Illum numquam facilis veniam, quis
adipisci commodi iste voluptatum in quos libero, obcaecati mollitia.
Deleniti earum odit vitae saepe quos culpa nisi dicta accusantium
animi in dignissimos, neque alias a?
</p>
</section>
<!-- <video
src="https://assets.mixkit.co/videos/preview/mixkit-stars-in-space-1610-large.mp4"
controls
preload="none"
></video> -->
<video data-src="https://assets.mixkit.co/videos/preview/mixkit-stars-in-space-1610-large.mp4" controls></video>
</main>
И...
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Montserrat", sans-serif;
font-size: 1rem;
}
:root {
--main-light: #f0f0f0;
--main-dark: #3c3c3c;
--main-blue: #c5dfee;
--active-blue: #53acff;
}
body {
background: var(--main-light);
color: var(--main-dark);
}
main {
margin: 0 auto;
padding: 1rem;
width: 90%;
text-indent: 1.25rem;
}
h2 {
font-size: 2rem;
margin: 1rem 0;
}
p {
font-size: 1.1rem;
}
img {
width: 90%;
margin: 1rem auto;
}
audio {
display: block;
margin: 1rem auto;
border-radius: 4px;
}
audio:focus-visible {
outline: none;
}
video {
width: 90%;
display: block;
margin: 1rem auto;
}
Думаю, вы уже успели составить себе примерное представление о том, как будет выглядеть код нашего скрипта. Для демонстрации дополнительных возможностей IOA
в сравнении с preload
, предлагаю запускать автоматическое воспроизведение аудио и видео при пересечении ими порогового значения (которое мы также установим в 30%):
// создаем функцию
const initLazyLoading = (root) => {
// находим все "ленивые" элементы
const lazyEls = [...root.querySelectorAll("[data-src]")]
// для каждого из них
lazyEls.forEach((el) => {
// создаем наблюдателя
new IntersectionObserver(
(entries) => {
// аудио или видео?
const audioOrVideo =
el.localName === "audio" || el.localName === "video"
// если целевой элемент находится в зоне пересечения
if (entries[0].isIntersecting) {
// извлекаем `src` из `data-src`
const { src } = el.dataset
if (!src) return
// если у элемента нет атрибута `src`
if (!el.hasAttribute("src")) {
// добавляем ему данный атрибут со значением `data-src`
el.setAttribute("src", src)
// удаляем `data-src`
el.removeAttribute("data-src")
}
if (audioOrVideo) {
// запускаем воспроизведение
el.play()
}
} else {
if (audioOrVideo) {
// останавливаем воспроизведение
el.pause()
}
}
},
{
// пороговое значение
threshold: 0.3
}
// начинаем наблюдать за элементом
).observe(el)
})
}
// вызываем функцию
initLazyLoading(document.querySelector("main"))
Мы не снимаем наблюдение с элементов посредством вызова метода unobserve
в колбэке, поскольку хотим запускать и останавливать воспроизведение аудио и видео при выполнении прокрутки страницы в обоих направлениях. Однако благодаря проверке if (!el.hasAttribute("src"))
атрибут data-src
меняется на атрибут src
только один раз.
Поиграть с этим кодом можно здесь:
До тех пор, пока медиа находятся за пределами области просмотра (проигнорируем пороговое значение подобно константе в большом "O"), они загружаться не будут. В этом мы можем убедиться, изучив вкладку Network
.
Изображение находится за пределами viewport
:
А вот мы до него "докрутили":
При попадании аудио и видео в область просмотра, начинается их автоматическое воспроизведение, которое автоматически останавливается при выходе соответствующих элементов из viewport
.
Рекомендую взглянуть на библиотеку vanilla-lazyload
. В дополнение к реализованному нами функционалу, она позволяет лениво загружать фоновые изображения, разные медиаресурсы в зависимости от ширины области просмотра (source
и srcset
) и т.д.
Вывод
В данной статье мы с вами рассмотрели всего лишь 2 примера практического использования IOA
. На самом деле, возможности, предоставляемые данным интерфейсом, намного шире. Парочку идей можно найти в этой статье. А здесь можно почитать о других "наблюдателях".
Надеюсь, вы не зря потратили время и нашли для себя что-то интересное.
Благодарю за внимание и хорошего дня!
Комментарии (19)
eggstream
27.08.2021 14:17+1Создавать обработчик события на каждый из разделов и на каждую из картинок?
А если всё это добро еще и Single Page…
Очень быстро мы получим что-то медленное и протекающее.
Может лучше по-старинке? С одним обработчиком события прокрутки на всю страницу, который отлавливает позиции нужных элементов и работает без полифиллов на браузерах 20-летней давности.
А ссылки сделать на старых добрых якорях, чтобы работало вообще без скриптов.JustDont
27.08.2021 15:42Подход "каждый отдельный кусок страницы чего-то там отдельно мониторит и сам подстраивается" на самом деле прекрасно работает и не то, чтоб прям принципиально медленный относительно централизованного обработчика.
При одном важном условии: что это всё нормально написано, что обработчики сами по себе быстрые и не пытаются делать тяжелые действия просто потому, что кому-то было лень нормально код структурировать. Что всё обязательно сносится при сносе блоков, и не утекает память. И вот тут да, я с вами согласен в том плане, что децентрализованный подход даёт куда больше способов самому себе отстрелить ногу. Но это не минус подхода, а минус конкретной его кривой реализации.
Gamma1st
08.09.2021 18:40Тут скорее вопрос в том, почему автор для решил для каждого элемента создавать свой инстанс Intersection Observer. Данное API позволяет отслеживать несколько элементов средствами одного инстанса.
derikn_mike
27.08.2021 19:49-2"а там, где поддерживается, срабатывает не всегда корректно "
интересно в каком году вы живете?
ivan386
28.08.2021 05:15По поводу имитации ленивой загрузки как мне кажется лучше наверно удалять скриптом атрибуты src после того как скрипт определит что ленивая загрузка не работает в браузере. И потом возвращать его когда нужно. В таком случае страница не останется без изображений при отключенном JavaScript у пользователя.
Для перехода между разделами сайта тоже есть естественное решение в виде якоря. Задаём разделу атрибут id (
<section id="1">
) и делаем ссылку на него(<a href="#1">Section 1</a>
). И опять же навигация будет работать без JavaScript. А в случае если он включен клик на ссылку можно перехватывать и делать навигацию красиво.
WanSpi
Что то у меня ваш первый пример не работает, а разве не проще было просто сделать коллбек для скролла, и проверять положения каждого блока с помощью getBoundingClientRect ?
UPD: работает, только надо перейти на codepen, во фрейме почему то не хочет, но все равно работает через раз, может спокойно перескролить некоторые секции в меню
wallydnb
Ваше решение менее производительное, так как вызывает ненужные forced layout\reflow
WanSpi
Я бы не сказал что менее производительней, частенько использую getBoundingClientRect для мониторинга, и на тех же мобильных пяти летней давности никаких проблем не было, все работает шустро, к тому же мой пример дает возможность точно понимать где сейчас находиться блок, в отличие от примера автора.
wallydnb
Как только у вас появятся сложный layout, который действительно будет долго пересчитывать, разница станет очевидной.
Тут вы правы, данное апи не покрывает всех кейсов.
WanSpi
Я конечно могу сильно ошибаться, так как не заглядывал в код движка браузера, того же хромиума, но разве просчет всех элементов, от положения, до их размеров, не идет перед рендерингом страницы? А значит я вытягиваю данные уже просчитанные, так что не до конца понимаю как это может замедлить выполнения кода.
Ну это уже проблема самого layout'a, не нужно громоздить миллионы DOM элементов на странице.
wallydnb
Это правда
Это неправда. При изменении некоторых свойств DOM элементов или вызове некоторых методов происходит приостановка выполнения кода и forced reflow.
WanSpi
Согласен, но я ведь не меняю их свойства, а лишь считываю, хотя может вы и правы, на счет данной функции я сильно не читал, лишь говорю с практики, углублюсь в ближайшее время в данную тему.
wallydnb
Чтение некоторых свойств тоже вызывает forced reflow. Как пример scrollTop.
WanSpi
Надо бы почитать, не думал что get может тоже вызвать forced reflow, вроде уже сижу на ваниле более 10 лет, думал что уже знаю практически все, но временами находиться моменты, о которых даже не подозревал, век живи, век учись :)
webschik
Вот здесь хорошая подборка свойств, вызывающих layout/reflow https://gist.github.com/paulirish/5d52fb081b3570c81e3a
WanSpi
Спасибо, хотя если честно, даже не до конца понимаю почему идет вызов forced reflow, понимаю при "elem.focus()", но что такого дает "elem.getBoundingClientRect()" ?
TheShock
Вы в потоке, добавили пару елементов, ещё паре элементов изменили стили, но оно ещё не сделало рефлоу, т.к. ваш поток с логикой не закончился и рендер ещё не начался. И тут вы говорите: "а ну ка дай мне размеры и положение такого то блока". Для того, чтобы узнать это значение - нужно применить все ваши изменения. Потому что посчитано то, что было в предыдущем кадре, но не на текущий момент.
WanSpi
То есть вы мне хотите сказать, что браузер не проверяет ли были сделаны изменения, и при каждом вызове elem.getBoundingClientRect() делает forced reflow ?
TheShock
Эмс. Где вы такое увидели вообще? Я как раз писал про изменения.
И да, браузер может дёрнуть рефлоу несколько раз за кадр (то есть в несколько раз больше, чем нужно) если вы изменяете-получаете-изменяете-получаете.