Привет, друзья!


На днях мне посчастливилось заниматься решением 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)


  1. WanSpi
    27.08.2021 13:54
    +1

    Что то у меня ваш первый пример не работает, а разве не проще было просто сделать коллбек для скролла, и проверять положения каждого блока с помощью getBoundingClientRect ?

    UPD: работает, только надо перейти на codepen, во фрейме почему то не хочет, но все равно работает через раз, может спокойно перескролить некоторые секции в меню


    1. wallydnb
      27.08.2021 15:49

      Ваше решение менее производительное, так как вызывает ненужные forced layout\reflow


      1. WanSpi
        27.08.2021 15:55

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


        1. wallydnb
          27.08.2021 17:29

          Как только у вас появятся сложный layout, который действительно будет долго пересчитывать, разница станет очевидной.

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

          Тут вы правы, данное апи не покрывает всех кейсов.


          1. WanSpi
            27.08.2021 17:46

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

            Как только у вас появятся сложный layout, который действительно будет долго пересчитывать, разница станет очевидной.

            Ну это уже проблема самого layout'a, не нужно громоздить миллионы DOM элементов на странице.


            1. wallydnb
              27.08.2021 18:41

              но разве просчет всех элементов, от положения, до их размеров, не идет перед рендерингом страницы

              Это правда
              А значит я вытягиваю данные уже просчитанные

              Это неправда. При изменении некоторых свойств DOM элементов или вызове некоторых методов происходит приостановка выполнения кода и forced reflow.


              1. WanSpi
                27.08.2021 18:56

                При изменении некоторых свойств DOM элементов или вызове некоторых методов происходит приостановка выполнения кода и forced reflow.

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


                1. wallydnb
                  27.08.2021 19:14

                  Чтение некоторых свойств тоже вызывает forced reflow. Как пример scrollTop.


                  1. WanSpi
                    27.08.2021 19:20

                    Надо бы почитать, не думал что get может тоже вызвать forced reflow, вроде уже сижу на ваниле более 10 лет, думал что уже знаю практически все, но временами находиться моменты, о которых даже не подозревал, век живи, век учись :)


                    1. webschik
                      27.08.2021 22:06
                      +2

                      Вот здесь хорошая подборка свойств, вызывающих layout/reflow https://gist.github.com/paulirish/5d52fb081b3570c81e3a


                      1. WanSpi
                        27.08.2021 22:11

                        Спасибо, хотя если честно, даже не до конца понимаю почему идет вызов forced reflow, понимаю при "elem.focus()", но что такого дает "elem.getBoundingClientRect()" ?


                      1. TheShock
                        28.08.2021 18:08
                        +1

                        Вы в потоке, добавили пару елементов, ещё паре элементов изменили стили, но оно ещё не сделало рефлоу, т.к. ваш поток с логикой не закончился и рендер ещё не начался. И тут вы говорите: "а ну ка дай мне размеры и положение такого то блока". Для того, чтобы узнать это значение - нужно применить все ваши изменения. Потому что посчитано то, что было в предыдущем кадре, но не на текущий момент.


                      1. WanSpi
                        30.08.2021 10:19

                        То есть вы мне хотите сказать, что браузер не проверяет ли были сделаны изменения, и при каждом вызове elem.getBoundingClientRect() делает forced reflow ?


                      1. TheShock
                        03.09.2021 04:47

                        Эмс. Где вы такое увидели вообще? Я как раз писал про изменения.

                        И да, браузер может дёрнуть рефлоу несколько раз за кадр (то есть в несколько раз больше, чем нужно) если вы изменяете-получаете-изменяете-получаете.


  1. eggstream
    27.08.2021 14:17
    +1

    Создавать обработчик события на каждый из разделов и на каждую из картинок?
    А если всё это добро еще и Single Page…
    Очень быстро мы получим что-то медленное и протекающее.
    Может лучше по-старинке? С одним обработчиком события прокрутки на всю страницу, который отлавливает позиции нужных элементов и работает без полифиллов на браузерах 20-летней давности.
    А ссылки сделать на старых добрых якорях, чтобы работало вообще без скриптов.


    1. JustDont
      27.08.2021 15:42

      Подход "каждый отдельный кусок страницы чего-то там отдельно мониторит и сам подстраивается" на самом деле прекрасно работает и не то, чтоб прям принципиально медленный относительно централизованного обработчика.
      При одном важном условии: что это всё нормально написано, что обработчики сами по себе быстрые и не пытаются делать тяжелые действия просто потому, что кому-то было лень нормально код структурировать. Что всё обязательно сносится при сносе блоков, и не утекает память. И вот тут да, я с вами согласен в том плане, что децентрализованный подход даёт куда больше способов самому себе отстрелить ногу. Но это не минус подхода, а минус конкретной его кривой реализации.


    1. Gamma1st
      08.09.2021 18:40

      Тут скорее вопрос в том, почему автор для решил для каждого элемента создавать свой инстанс Intersection Observer. Данное API позволяет отслеживать несколько элементов средствами одного инстанса.


  1. derikn_mike
    27.08.2021 19:49
    -2

    "а там, где поддерживается, срабатывает не всегда корректно "

    интересно в каком году вы живете?


  1. ivan386
    28.08.2021 05:15

    По поводу имитации ленивой загрузки как мне кажется лучше наверно удалять скриптом атрибуты src после того как скрипт определит что ленивая загрузка не работает в браузере. И потом возвращать его когда нужно. В таком случае страница не останется без изображений при отключенном JavaScript у пользователя.


    Для перехода между разделами сайта тоже есть естественное решение в виде якоря. Задаём разделу атрибут id (<section id="1">) и делаем ссылку на него(<a href="#1">Section 1</a>). И опять же навигация будет работать без JavaScript. А в случае если он включен клик на ссылку можно перехватывать и делать навигацию красиво.