> Как реализовать бесконечный скролл (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: TYMY

Стек: JavaScript

> Пример ответа

Короткий ответ

Бесконечный скролл реализуется через Intersection Observer API или обработчик scroll с throttling. Основная идея - отслеживать, когда пользователь достигает конца списка, и подгружать следующую порцию данных. Ключевые моменты: управление состоянием загрузки, предотвращение дублирующих запросов, корректная обработка ошибок и очистка слушателей при размонтировании компонента.

Подробное объяснение

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

  1. Intersection Observer API - современный, производительный способ. Создаётся наблюдатель за элементом-триггером (обычно последний элемент списка или специальный sentinel). Когда триггер пересекает viewport, вызывается callback для загрузки данных. Observer автоматически оптимизирует проверки, не нагружая main thread.

  2. Scroll event + throttling - классический подход. Навешивается обработчик scroll на контейнер или window, проверяется scrollTop + clientHeight >= scrollHeight. Для избежания частых вызовов используется throttle (например, 100-200ms). Минус - может вызывать layout thrashing при частых чтениях геометрии.

  3. Virtual scrolling - для очень больших списков (тысячи элементов) используется виртуализация: рендерятся только видимые элементы, а DOM-дерево не перегружается. Бесконечный скролл в этом случае комбинируется с подгрузкой новых данных при приближении к концу буфера.

Ключевые аспекты реализации:

  • Управление состоянием: флаги isLoading, hasMore, error. Предотвращение повторных запросов, пока текущий не завершён.
  • Обработка ошибок: при ошибке загрузки показывать сообщение и кнопку "Повторить", не блокируя дальнейший скролл.
  • Очистка ресурсов: отключение observer или удаление обработчика при unmount компонента.
  • Debounce/Throttle: для scroll-подхода - throttle, для поиска - debounce.
  • Оптимизация рендера: мемоизация списка, использование React.memo или key для эффективного обновления.

На практике

В современных React-приложениях предпочтительнее Intersection Observer. Типичная реализация:

  1. Создаётся ref для sentinel-элемента (обычно div в конце списка).
  2. В useEffect инициализируется IntersectionObserver с rootMargin: '100px' (подгрузка за 100px до конца).
  3. При пересечении вызывается функция загрузки, если hasMore и не isLoading.
  4. После успешной загрузки данные добавляются в стейт, hasMore обновляется на основе ответа сервера.
  5. При ошибке устанавливается флаг error, показывается UI для повторной попытки.
  6. В cleanup observer отключается.

Для scroll-подхода важно использовать passive: true и throttle из lodash или собственный. Также нужно учитывать, что scroll-событие может не сработать при быстрой прокрутке - Intersection Observer лишён этого недостатка.

Пример кода

JAVASCRIPT
import { useRef, useEffect, useState, useCallback } from 'react';
function InfiniteScrollList() {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [error, setError] = useState(null);
const sentinelRef = useRef(null);
const loadMore = useCallback(async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
setError(null);
try {
const newItems = await fetchItems(items.length, 20);
setItems(prev => [...prev, ...newItems]);
setHasMore(newItems.length === 20);
} catch (err) {
setError('Ошибка загрузки');
} finally {
setIsLoading(false);
}
}, [isLoading, hasMore, items.length]);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) loadMore();
},
{ rootMargin: '100px' }
);
if (sentinelRef.current) observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, [loadMore]);
return (
<div>
{items.map((item, idx) => (
<div key={item.id}>{item.name}</div>
))}
<div ref={sentinelRef} style={{ height: 1 }} />
{isLoading && <div>Загрузка...</div>}
{error && <button onClick={loadMore}>Повторить</button>}
</div>
);
}

Как отвечать на собеседовании

Начни с выбора основного подхода - Intersection Observer, объясни почему он предпочтительнее scroll-события (производительность, простота). Упомяни управление состоянием (isLoading, hasMore, error) и важность очистки. Для senior-уровня добавь про виртуализацию для больших списков, про обработку race conditions (если пользователь быстро скроллит), про оптимизацию с useCallback/useMemo. Если спросят про scroll - расскажи про throttle и passive listener. Покажи понимание trade-off между подходами.

Что проверяет интервьюер

  • Понимание современных API браузера (Intersection Observer).
  • Умение управлять асинхронными состояниями (загрузка, ошибка, завершение).
  • Знание принципов оптимизации производительности (throttle, observer, очистка).
  • Способность предотвращать баги (дублирующие запросы, утечки памяти).
  • Понимание UX-нюансов (prefetch за границей экрана, обработка ошибок).

Типичные ошибки

  • Отсутствие проверки isLoading - отправка множества запросов при быстром скролле.
  • Забытая очистка observer или listener - утечка памяти и баги после unmount.
  • Использование scroll-события без throttle - падение FPS.
  • Неправильный rootMargin - подгрузка слишком поздно или слишком рано.
  • Игнорирование ошибок - список зависает без возможности повтора.
  • Отсутствие key или неправильные key в списке - перерендер всех элементов.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью