Backend

Как сделать бесконечный скролл на Intersection Observer API без библиотек

Как сделать бесконечный скролл на Intersection Observer API без библиотек

Когда речь заходит о реализации бесконечного скролла (Infinite Scroll), многие новички по привычке тянутся к событию window.onscroll. Это прямой путь к деградации производительности вашего приложения. Постоянные вызовы обработчика при каждом пикселе прокрутки забивают основной поток (Main Thread), что приводит к «фризам» интерфейса, особенно на мобильных устройствах.

В современном вебе есть гораздо более элегантное и производительное решение — Intersection Observer API. В этой статье я разберу, как собрать надежный механизм подгрузки контента с нуля, используя только чистый JavaScript.

Почему Intersection Observer, а не scroll-event?

Прежде чем переходить к коду, давайте разберемся в матчасти. Основная проблема onscroll в том, что он синхронен. Чтобы понять, достиг ли пользователь низа страницы, вам нужно вручную вычислять scrollTop, offsetHeight и scrollHeight.

Intersection Observer работает иначе: он позволяет браузеру асинхронно уведомлять ваш код о том, что определенный элемент (цель) пересек границы другого элемента (корня) или окна просмотра.

Преимущества такого подхода:

    1. Производительность: Браузер оптимизирует проверку пересечений, не нагружая CPU постоянными вычислениями.
    1. Чистота кода: Вам не нужно писать сложные математические формулы для расчета координат.
    1. Гибкость: Легко настроить «порог» срабатывания (например, начать загрузку, когда до конца списка осталось еще 200 пикселей).

Архитектура решения

Для реализации бесконечного скролла нам понадобятся три составляющие:

  1. Контейнер с данными — место, куда мы будем рендерить элементы.
  2. Sentinel (Страж) — пустой невидимый элемент в самом низу списка. Именно за ним будет следить наш наблюдатель.
  3. Логика загрузки — функция, которая делает запрос к API и добавляет новые данные в DOM.

Использование «стража» вместо отслеживания последнего элемента списка — это хороший тон. Это позволяет избежать проблем, если список изначально пуст или содержит всего один элемент.

Пошаговая реализация

Шаг 1: Разметка HTML

Нам нужен простой список и тот самый элемент-индикатор в конце.

Шаг 2: Создание функции получения данных

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

javascript
let page = 1;
const limit = 10;

async function fetchPosts() {
const response = await fetch(https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${limit});
const data = await response.json();
return data;
}

Шаг 3: Рендеринг контента

Напишем функцию, которая превращает массив объектов в HTML-элементы.

javascript
function renderPosts(posts) {
const container = document.getElementById(‘post-container’);

posts.forEach(post => {
    const postElement = document.createElement('div');
    postElement.classList.add('post-card');
    postElement.innerHTML = `
        <h3>${post.id}. ${post.title}</h3>
        <p>${post.body}</p>
    `;
    container.appendChild(postElement);
});

}

Шаг 4: Настройка Intersection Observer

Это сердце нашего функционала. Нам нужно создать экземпляр наблюдателя и привязать его к нашему load-more-trigger.

javascript
const trigger = document.getElementById(‘load-more-trigger’);
const loader = document.getElementById(‘loader’);

const observerOptions = {
root: null, // следим относительно окна браузера (viewport)
rootMargin: ‘0px’,
threshold: 0.1 // сработает, когда хотя бы 10% элемента появятся в зоне видимости
};

const observer = new IntersectionObserver(async (entries) => {
const entry = entries[0];

if (entry.isIntersecting) {
    // Показываем лоадер
    loader.style.display = 'block';

    try {
        const posts = await fetchPosts();

        if (posts.length > 0) {
            renderPosts(posts);
            page++; // Увеличиваем страницу для следующего запроса
        } else {
            // Если данных больше нет, отключаем наблюдение
            observer.unobserve(trigger);
            loader.innerHTML = 'Все посты загружены!';
        }
    } catch (error) {
        console.error('Ошибка при загрузке:', error);
    } finally {
        loader.style.display = 'none';
    }
}

}, observerOptions);

// Запускаем слежку за стражем
observer.observe(trigger);

Важные нюансы и «подводные камни»

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

1. Защита от дублирующих запросов (Race Condition)

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

Решение: Введите флаг isLoading.

javascript
let isLoading = false;

const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !isLoading) {
isLoading = true;
// … логика fetch …
isLoading = false;
}
}, observerOptions);

2. Оптимизация rootMargin

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

Например, rootMargin: '200px' заставит наблюдатель сработать, когда элемент-страж окажется в 200 пикселях от нижнего края экрана. Это создает эффект действительно «бесконечной» ленты.

3. Очистка памяти

Если ваш бесконечный скролл находится внутри Single Page Application (React, Vue, Angular), не забывайте вызывать observer.disconnect(), когда компонент размонтируется. В противном случае вы получите утечку памяти.

Итоговый чек-лист реализации

Чтобы ваш бесконечный скролл работал стабильно, проверьте следующее:

  1. Асинхронность: Используется ли IntersectionObserver вместо onscroll?
  2. Индикатор: Есть ли визуальный фидбек (loader) для пользователя?
  3. Пагинация: Корректно ли передается номер страницы или смещение (offset) на сервер?
  4. Предохранитель: Реализована ли блокировка повторных запросов через флаг isLoading?
  5. Конец списка: Есть ли проверка на пустой ответ от сервера для остановки наблюдателя (unobserve)?

Заключение

Intersection Observer API — это стандарт индустрии для решения задач, связанных с видимостью элементов. Он позволяет перенести тяжелые вычисления по расчету координат с JavaScript-потока на уровень браузера, что делает интерфейс плавным и отзывчивым.

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