RUVDS.com corporate blog
Website development
JavaScript
May 31

API IntersectionObserver и ленивая загрузка изображений

Original author: Chidume Nnamdi
Translation
Использование ленивой загрузки изображений для улучшения производительности веб-проектов — востребованная техника оптимизации. Всё дело в том, что изображения — это «тяжёлые» ресурсы, которыми переполнены современные веб-сайты. Мы уже кое-что об этом публиковали. Здесь можно почитать о том, что дала ленивая загрузка сайту Walmart, и узнать о том, как пользоваться IntersectionObserver в React-проектах. Вот статья об оптимизация статических сайтов. Вот недавний материал о реализации ленивой загрузки средствами браузера.



Сегодня мы представляем вашему вниманию перевод статьи, в которой использование API IntersectionObserver рассмотрено на примере простой веб-страницы. Этот материал рассчитан на начинающих программистов.

Что такое ленивая загрузка изображений?


При анализе производительности приложений на первый план выступают два показателя — время до первой интерактивности (Time To Interactive) и потребление ресурсов (Resources Consumption). С ними неизбежно придётся столкнуться тем, кто занимается веб-разработкой. Кроме того, проблемы с приложениями могут возникать не только из-за того, что они долго готовятся к работе или потребляют слишком много ресурсов. Но, в любом случае, очень важно как можно раньше находить источники этих проблем и стремиться к тому, чтобы проблемы даже и не возникали.

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

  1. Время до первой интерактивности. Это — время, которое нужно веб-приложению для загрузки и приведения интерфейса в состояние, пригодное для работы с ним пользователя. Ленивая загрузка (причём, речь идёт не только об изображениях) оптимизирует время отклика приложений благодаря технологиям разделения кода и загрузки только того, что нужно конкретной странице, или того, что нужно в некий конкретный момент времени.
  2. Потребление ресурсов. Люди — существа нетерпеливые. Если веб-сайту нужно больше 3 секунд на то, чтобы загрузиться, 70% пользователей с такого сайта уходят. Веб-приложения не должны загружаться так долго. Ленивая загрузка позволяет уменьшить объём ресурсов, необходимых для работы страниц. Речь, например, может идти о том, что код некоего проекта разбивается на фрагменты, которые загружаются только на тех страницах, которые в них нуждаются. В результате растёт производительность сайта и снижается потребление системных ресурсов.

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

Вот пара преимуществ, которые даёт веб-проектам ленивая загрузка:

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

Ленивая загрузка изображений с использованием API IntersectionObserver


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

Рассмотрим пример.


Страница и её видимая область

Взгляните на предыдущий рисунок. Тут можно видеть браузер и загружаемую в него веб-страницу. Изображения #IMG_1 и #IMG_2 находятся в области видимости страницы. Это означают, что они видимы пользователю и находятся в границах той области окна браузера, которая выводится на экран.

Нельзя признать идеальным такой порядок работы со страницей, когда при её загрузке сразу же загружаются и изображения #IMG_1, #IMG_2, #IMG_3 и #IMG_4. Пользователю видны лишь #IMG_1 и #IMG_2, а #IMG_3 и #IMG_4 от него скрыты. Если при загрузке страницы загрузить первое и второе изображения, а третье и четвёртое не загружать — это могло бы оказать позитивное влияние на производительность сайта. А именно, речь идёт о следующем. Когда пользователь прокручивает страницу так, что видимым становится третье изображение — оно загружается. Если прокрутка продолжается и видимым становится четвёртое изображение — оно тоже загружается.


Прокрутка страницы и загрузка изображений

Теперь, когда мы выяснили, как именно мы хотим работать с изображениями, зададимся вопросом о том, как узнать о том, что некий элемент страницы попадает в её видимую область. В современных браузерах имеется API, позволяющий программисту узнать о том, когда некая область страницы становится видимой. Речь идёт об API IntersectionObserver. Он позволяет, пользуясь асинхронными механизмами, организовать наблюдение за элементами и получать уведомления в тех случаях, когда элемент пересекает область видимости документа или пересекает границу другого элемента.

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

<img class="lzy_img" src="lazy_img.jpg" data-src="real_img.jpg" />

Класс позволяет идентифицировать элемент как изображение, к которому будет применяться ленивая загрузка. Атрибут src позволяет вывести изображение-заполнитель до вывода реального изображения. В data-src хранится адрес реального изображения, которое будет загружено при попадании элемента в видимую область страницы.

Теперь напишем код, реализующий ленивую загрузку. Как уже было сказано, для обнаружения момента попадания изображения в область просмотра страницы мы будем пользоваться API IntersectionObserver.

Для начала создадим экземпляр IntersectionObserver:

const imageObserver = new IntersectionObserver(...);

Конструктор IntersectionObserver принимает функцию с двумя параметрами. Один из них хранит массив, состоящий из элементов, за которыми нужно организовать наблюдение, другой — экземпляр IntersectionObserver. Выглядит это так:

const imageObserver = new IntersectionObserver((entries, imgObserver) => {
    entries.forEach((entry) => {
        //...
    })
});

В коде функции осуществляется проверка того, пересекают ли изображения, представленные элементами массива entries, область просмотра. Если это так — то в атрибут src соответствующего изображения записывается то, что было в его атрибуте data-src.

const imageObserver = new IntersectionObserver((entries, imgObserver) => {
    entries.forEach((entry) => {
        if(entry.isIntersecting) {
            const lazyImage = entry.target
            lazyImage.src = lazyImage.dataset.src
        }
    })
});

Тут мы проверяем, с помощью условия if(entry.isIntersecting) {...}, пересекает ли элемент область просмотра браузера. Если это так — мы сохраняем элемент img в константе lazyImage. Затем записываем в его атрибут src то, что было в его атрибуте data-src. Благодаря этому изображение, адрес которого хранится в data-src, загружается и выводится на экран. В браузере это выглядит как замена изображения-заполнителя, lazy_img.jpg, на реальное изображение.

Теперь нужно воспользоваться методом .observe() нашего экземпляра IntersectionObserver для того чтобы начать наблюдение за интересующими нас элементами:

imageObserver.observe(document.querySelectorAll('img.lzy_img'));

Здесь мы выбираем из документа все элементы img с классом lzy_img командой document.querySelectorAll('img.lzy_img') и передаём их методу .observe(). Он, получив список элементов, запускает процесс наблюдения за ними.

Для того чтобы испытать этот пример начнём с инициализации Node.js-проекта:

mkdir lzy_img
cd lzy_img
npm init -y

Теперь создадим в папке lzy_img файл index.html:

touch index.html

Добавим в него следующий код:

<html>
<title>Lazy Load Images</title>
<body>
    <div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_1.jpg" />
            <hr />
        </div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_2.jpg" />
            <hr />
        </div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_3.jpg" />
            <hr />
        </div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_4.jpg" />
            <hr />
        </div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_5.jpg" />
            <hr />
        </div>
    </div>
    <script>
        document.addEventListener("DOMContentLoaded", function() {
            const imageObserver = new IntersectionObserver((entries, imgObserver) => {
                entries.forEach((entry) => {
                    if (entry.isIntersecting) {
                        const lazyImage = entry.target
                        console.log("lazy loading ", lazyImage)
                        lazyImage.src = lazyImage.dataset.src
                    }
                })
            });
            const arr = document.querySelectorAll('img.lzy_img')
            arr.forEach((v) => {
                imageObserver.observe(v);
            })
        })
    </script>
</body>
</html>

Можно заметить, что тут описаны 5 изображений, в которых при загрузке выводится заполнитель lazy_img.jpg. В атрибуте data-src каждого из них содержатся сведения о реальных изображениях. Вот список имён изображений, используемых в проекте:

lazy_img.jpg
img_1.jpg
img_2.jpg
img_3.jpg
img_4.jpg
img_5.jpg

Все эти изображения вам надо создать самостоятельно, приведя папку проекта к виду, показанному на следующем рисунке.


Папка проекта

В моём случае файл lazy_img.jpg подготовлен средствами Windows Paint, а реальные изображения (img_*.jpg) взяты с pixabay.com.

Обратите внимание на то, что в коллбэке, использованном при создании экземпляра IntersectionObserver, есть вызов console.log(). Это позволит нам узнать о выполнении операций по загрузке разных изображений.

Теперь, чтобы обслуживать index.html, воспользуемся пакетом http-server:

npm i http-server

Добавим свойство start в раздел scripts файла package.json:

"scripts": {
    "start": "http-server ./"
}

После этого выполним в терминале команду npm run start.

Теперь открываем браузер и переходим по адресу 127.0.0.1:8080. Наша страница index.html в самом начале будет выглядеть примерно так, как показано на следующем рисунке.


Страница без реальных изображений

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


Браузер загрузил первое изображение

Другие изображения пока не загружены. Они ещё не попали в область просмотра.

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


Многократная загрузка одного и того же изображения

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

Для того чтобы решить эту проблему нам нужно убирать элемент img, настоящее изображение для которого уже загружено, из списка элементов, за которыми наблюдает наш экземпляр IntersectionObserver. Также нам нужно убирать класс lzy_img из этого элемента. Вот как это выглядит в отредактированном коде функции обратного вызова:

<script>
    document.addEventListener("DOMContentLoaded", function() {
        const imageObserver = new IntersectionObserver((entries, imgObserver) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                    const lazyImage = entry.target
                    console.log("lazy loading ", lazyImage)
                    lazyImage.src = lazyImage.dataset.src
                    lazyImage.classList.remove("lzy_img");
                    imgObserver.unobserve(lazyImage);
                }
            })
        });
        const arr = document.querySelectorAll('img.lzy_img')
        arr.forEach((v) => {
            imageObserver.observe(v);
        })
    })
</script>

После загрузки реального изображения соответствующего элемента img удаляется его класс lzy_img и элемент исключается из списка элементов, за которыми наблюдает IntersectionObserver.

Вот готовый код примера:

<html>
<title>Lazy Load Images</title>
<body>
    <div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_1.jpg" />
            <hr />
        </div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_2.jpg" />
            <hr />
        </div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_3.jpg" />
            <hr />
        </div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_4.jpg" />
            <hr />
        </div>
        <div style="">
            <img class="lzy_img" src="lazy_img.jpg" data-src="img_5.jpg" />
            <hr />
        </div>
    </div>
    <script>
        document.addEventListener("DOMContentLoaded", function() {
            const imageObserver = new IntersectionObserver((entries, imgObserver) => {
                entries.forEach((entry) => {
                    if (entry.isIntersecting) {
                        const lazyImage = entry.target
                        console.log("lazy loading ", lazyImage)
                        lazyImage.src = lazyImage.dataset.src
                        lazyImage.classList.remove("lzy_img");
                        imgObserver.unobserve(lazyImage);
                    }
                })
            });
            const arr = document.querySelectorAll('img.lzy_img')
            arr.forEach((v) => {
                imageObserver.observe(v);
            })
        })
    </script>
</body>
</html>

Итоги


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

Уважаемые читатели! Есть ли у вас примеры улучшения производительности веб-сайтов после внедрения системы ленивой загрузки изображений?

+30
4.9k 73
Comments 22
Top of the day