Lazy loading

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

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

Что такое отложенная загрузка?

Самыми важными составляющими производительности приложений являются время отклика и потребление ресурсов.

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

  1. Время отклика — это время, которое требуется для того, чтобы веб-приложение загрузилось, а UI-интерфейс начал реагировать на запросы пользователей. Отложенная загрузка оптимизирует время отклика с помощью разделения кода и загрузки необходимого бандла.
  2. Потребление ресурсов. Если загрузка веб-сайта длится более трех секунд, 70% пользователей покидают веб-сайт. При отложенной загрузке объем загружаемых ресурсов сокращается благодаря загрузке только необходимого на данном этапе бандла кода.

Отложенная загрузка ускоряет время загрузки приложения, загружая ресурсы по запросу.

Преимущества отложенной загрузки:

  • Высокая производительность при начальной загрузке.
  • Загрузка меньшего количества ресурсов при начальной загрузке.

Отложенная загрузка изображений с помощью Intersection Observer

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

Рассмотрим изображение выше. Веб-браузер и веб-страница загружены. Изображения #IMG_1 и #IMG_2 находятся в окне просмотра (видны пользователю и находятся в рамках представления браузера).

Изображения #IMG_3 и #IMG_4 находятся вне зоны видимости пользователя. Таким образом, производительность сайта заметно улучшится, если #IMG_1 и #IMG_2 будут загружены в первую очередь, а #IMG_ 3 и #IMG_4 позже по очереди при прокрутке вниз.

Но как определить, когда элемент появляется в представлении? Современный браузер предоставляет новый Intersection Observer API, с помощью которого можно определить, когда элемент попадает в окно просмотра.

Intersection Observer

Intersection Observer API предоставляет асинхронное наблюдение изменений в видимости элементов или относительной видимости двух элементов по отношению друг к другу.

Для отложенной загрузки изображений нужно определить элемент с шаблоном разметки:

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

Так выглядит отложено загруженный элемент изображения.

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

Теперь переходим к написанию логики отложенной загрузки. Как было сказано выше, для определения видимости элемента по отношению к документу в браузере мы используем Intersection Observer.

Сначала создаем экземпляр Intersection Observer:

const imageObserver = new IntersectionObserver(...);

IO принимает функцию в конструкторе, которая обладает двумя параметрами: первый содержит массив, состоящий из элемента для наблюдения, а второй содержит экземпляр IO.

const imageObserver = new IntersectionObserver((entries, imgObserver) => {
    //...
});
  • entries: содержит в массиве элементы, находящиеся в окне просмотра браузера.
  • imgObserver: экземпляр IntersectionObserver.

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

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

Затем нужно проверить, находится ли каждый entry в окне просмотра. Если entry пересекается с окном просмотра, то устанавливаем значение атрибута data-src для атрибута src в элементе img.

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

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

Теперь нужно вызвать imageObserver для начала наблюдения за элементами img:

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

Объединяем все элементы с классом lzy_img в документе document.querySelectorAll('img.lzy_img') и передаем в imageObserver.observer(...).

imageObserver.observer(...) выбирает массив элементов и прослушивает их, чтобы узнать о пересечении их видимости с браузером.

Чтобы увидеть демо отложенной загрузки, нужно запустить проект Node:

mkdir lzy_img
cd lzy_img
npm init -y

Создайте файл 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, а также несколько реальных изображений для загрузки.

Необходимо создать свои изображения:

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.

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

Для использования файла index.html нужно установить http-сервер:

npm i http-server

Теперь добавляем свойство start в раздел сценариев в package.json.

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

Теперь запустите npm run start в терминале.

Откройте браузер и перейдите к 127.0.0.1:8080. Загруженный index.html будет выглядеть следующим образом:

Изображения показывают lazy_img.jpg, и поскольку <img class="lzy_img" src="lazy_img.jpg" data-src="img_1.jpg" /> находится в окне просмотра, то загружается реальное изображение img_1.jpg.

Другие изображения не загружаются, так как они не находятся в представлении браузера.

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

Это серьезно повлияет на производительность сайта.

Чтобы решить эту проблему, нужно отписать IntersectionObserver от img, для которого уже загружено реальное изображение, а также удалить класс 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")
                        imgObserver.unobserve(lazyImage)
                    }
                })
            });
            // ...
        })
    </script>

Полная версия кода

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

Заключение

Мы рассмотрели отложенную загрузку изображений с помощью IntersecionObserver API, а затем реализовали демо-версию с настройкой кода отложенной загрузки в JS.


Перевод статьи Chidume Nnamdi 🔥💻🎵🎮: Lazy Loading Images using the Intersection Observer API