Бесконечная прокрутка на HTML/CSS/JS

Бесконечная прокрутка (infinite scroll) — это подход, при котором контент подгружается автоматически по мере приближения пользователя к концу страницы. В отличие от классической нумерованной пагинации, здесь нет явных страниц и кнопок «Дальше». Такой подход делает навигацию плавной, но требует аккуратной реализации, чтобы не ухудшать производительность и UX.
Настройка фронтенда
Ниже — минимальная структура HTML для демонстрации бесконечной прокрутки. Страница содержит контейнер для карточек/изображений и подключает CSS и JS-файлы.
Infinite Scroll Page
Infinite Scroll Page
Такой стартовый шаблон подойдёт для развития функционала: подгрузки новых изображений, показа индикатора загрузки и обработки ошибок.
Стилизация — style.css
Приведённый CSS отображает карточки в гибкой сетке и задаёт простую стилизацию для индикатора загрузки.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html { font-size: 62.5%; }
body {
font-family: Cambria, Times, "Times New Roman", serif;
background: #fafafa;
color: #222;
padding-bottom: 6rem; /* пространство для индикатора */
}
h1 {
text-align: center;
font-size: 3.2rem;
padding: 2rem;
}
img {
width: 100%;
display: block;
border-radius: 6px;
}
.products__list {
display: flex;
flex-wrap: wrap;
gap: 2rem;
justify-content: center;
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.products__list > * {
width: calc(33% - 2rem);
min-width: 220px;
}
.loading-indicator {
display: none;
position: fixed;
bottom: 30px;
left: 50%;
background: rgba(0,0,0,0.8);
padding: 0.8rem 1.2rem;
color: #fff;
border-radius: 10px;
transform: translateX(-50%);
font-size: 1.4rem;
}
@media (max-width: 768px) {
.products__list > * { width: calc(50% - 2rem); }
}
@media (max-width: 480px) {
.products__list > * { width: 100%; }
}Основная реализация на JavaScript
Идея простая: отслеживать положение скролла или использовать IntersectionObserver; при приближении к низу страницы — запрашивать новые данные и добавлять их в DOM.
Простой пример с обработчиком scroll (подойдёт для демонстрации):
"use strict";
let isFetching = false; // флаг, чтобы избежать параллельных запросов
window.addEventListener("scroll", () => {
if (
window.scrollY + window.innerHeight >=
document.documentElement.scrollHeight - 100
) {
// Пользователь близок к низу — загружаем ещё
fetchMoreContent();
}
});
async function fetchMoreContent() {
if (isFetching) return;
isFetching = true;
try {
const response = await fetch("https://fakestoreapi.com/products?limit=3");
if (!response.ok) throw new Error("Network response was not ok");
const data = await response.json();
displayNewContent(data);
} catch (error) {
console.error("Ошибка при загрузке контента:", error);
} finally {
isFetching = false;
console.log("Fetch function finished");
}
}
const productsList = document.querySelector(".products__list");
function displayNewContent(data) {
data.forEach((item) => {
const imgElement = document.createElement("img");
imgElement.src = item.image;
imgElement.alt = item.title || "Изображение товара";
productsList.appendChild(imgElement);
});
}Выше — минимальная рабочая логика: fetch → render → сброс флага. На практике стоит добавить индикатор загрузки, обработку пустого результата и отложенный запуск (debounce/throttle).
Индикатор загрузки и улучшения UX
Добавьте в HTML элемент индикатора:
Загрузка...
Затем в JS выберите элемент и управляйте его видимостью:
const loadingIndicator = document.querySelector(".loading-indicator");
function showLoadingIndicator() {
loadingIndicator.style.display = "block";
}
function hideLoadingIndicator() {
loadingIndicator.style.display = "none";
}
async function fetchMoreContent() {
if (isFetching) return;
isFetching = true;
showLoadingIndicator();
try {
const response = await fetch("https://fakestoreapi.com/products?limit=3");
if (!response.ok) throw new Error("Network response was not ok");
const data = await response.json();
displayNewContent(data);
} catch (error) {
console.error("Ошибка при загрузке контента:", error);
} finally {
hideLoadingIndicator();
isFetching = false;
}
}Альтернатива: IntersectionObserver (рекомендуется)
IntersectionObserver отслеживает пересечение целевого элемента с viewport и обычно более эффективен, чем слушатель scroll. Используйте «сентинел» — пустой див в конце списка — и наблюдайте за ним.
// Создаём элемент-сентинел в конце списка
const sentinel = document.createElement('div');
sentinel.className = 'sentinel';
productsList.after(sentinel);
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
fetchMoreContent();
}
});
}, { rootMargin: '200px' });
observer.observe(sentinel);IntersectionObserver снижает нагрузку на поток событий и упрощает логику debounce.
Лучшие практики
- Не загружайте слишком много элементов за один запрос — используйте разумный лимит (например, 5–20 в зависимости от размера).
- Ограничивайте одновременные запросы флагом isFetching и/или AbortController для отмены старых запросов.
- Используйте debounce/throttle или IntersectionObserver, чтобы избежать сотен вызовов fetch при быстром скролле.
- Показывайте индикатор загрузки и понятные сообщения при отсутствии контента.
- Предоставьте альтернативу: кнопка “Загрузить ещё” или классическая пагинация для пользователей, которые её предпочитают.
- Учитывайте доступность: клавиатурная навигация, скринридеры и уведомления для пользователей с ограничениями.
Когда бесконечный скролл не подходит
- Если пользователи часто возвращаются к конкретным позициям в списке — пагинация даёт стабильные URL и якоря.
- Если важен SEO для содержимого глубокой вложенности — пагинация или server-side rendering с каноническими ссылками предпочтительнее.
- Для сложных фильтров и сортировки бесконечный поток может усложнить UX; рассмотрите смешанный подход.
Практические шаблоны и сниппеты (cheat sheet)
- Debounce-функция (100–300 мс): задержка перед вызовом fetch
function debounce(fn, wait = 200) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), wait);
};
}- Пример использования AbortController для отмены предыдущего запроса
let controller;
async function fetchWithAbort(url) {
if (controller) controller.abort();
controller = new AbortController();
const signal = controller.signal;
const res = await fetch(url, { signal });
return res.json();
}Роли и чеклист перед релизом
- Frontend-разработчик: проверить debounce/обработку ошибок, memory leaks, доступность фокуса.
- Backend-разработчик: поддержать пагинацию на API, корректные лимиты и заголовки кеширования.
- UX/UI дизайнер: предусмотреть кнопку «Загрузить ещё», индикацию загрузки и состояние «конец контента».
Короткий чеклист:
- Флаг isFetching или AbortController внедрён
- Показаны состояния: загрузка, ошибка, конец списка
- Тесты на медленном соединении
- Доступность: aria-атрибуты и фокус
Критерии приёмки
- При прокрутке загружаются новые элементы без дублирования.
- Наблюдаемый элемент (или scroll-лист) не генерирует больше одного параллельного запроса.
- Индикатор загрузки отображается во время запроса и исчезает при завершении.
- При достижении конца контента отображается сообщение «Больше нет элементов».
Короткая сводка
Бесконечная прокрутка повышает вовлечённость, но требует заботы о производительности и доступности. Выберите IntersectionObserver для производительности, используйте флаги/AbortController, показывайте индикатор загрузки и всегда предлагайте альтернативный способ навигации для тех, кто предпочитает пагинацию.
План действий для внедрения: 1) реализовать базовый fetch+render, 2) добавить isFetching/AbortController, 3) заменить scroll на IntersectionObserver, 4) покрыть UX/доступность и протестировать на реальных устройствах.
Важно: тестируйте на медленных сетях и мобильных устройствах, чтобы избежать негативного пользовательского опыта.
Похожие материалы
Голосовой PIN на Amazon Echo — как настроить
Как получить Apple TV+ бесплатно на 3 месяца
Запуск Windows‑игр на Apple Silicon с CrossOver
Извлечь текст из изображений и PDF через Google Drive
Как создать аватар в Facebook