Бесконечная прокрутка, пагинация или «Показать ещё»: что выбрать
Практика3 минРедакция Innovialabs
Коротко
Для каталогов и поиска, где важно найти и вернуться, выбирайте пагинацию или «Показать ещё» с адресами страниц; бесконечная прокрутка подходит лентам, которые просматривают без цели.
Короткий ответ
- Пагинация — когда пользователь ищет конкретное: каталог, результаты поиска, архив. Понятно, где он находится, легко вернуться и поделиться ссылкой.
- «Показать ещё» — компромисс для каталогов и блогов: подгрузка без перехода, но пользователь сам решает, когда грузить, и до футера можно дойти.
- Бесконечная прокрутка — для лент, которые листают без конкретной цели: соцсети, подборки, видео.
Если сомневаетесь, начните с «Показать ещё» + URL вида ?page=N: этот вариант реже всего ломает навигацию и SEO.
Сравнение
| Пагинация | «Показать ещё» | Бесконечная прокрутка | |
|---|---|---|---|
| UX | контроль и ориентир | контроль без перезагрузки | минимум усилий, легко потеряться |
| Футер | доступен | доступен | часто недостижим |
| Производительность | лёгкие страницы | DOM растёт по запросу | DOM растёт постоянно |
| SEO | лучший вариант | хорошо, если есть ссылки на страницы | плохо без отдельных URL |
| Аналитика | просмотры страниц из коробки | нужны события | нужны события и глубина прокрутки |
| Возврат «назад» | работает сам | нужно сохранять состояние | нужно сохранять состояние |
SEO: что учесть
Поисковые роботы не прокручивают страницу и не нажимают кнопки. Чтобы весь список был проиндексирован:
- у каждой порции должен быть свой URL (
/catalog?page=2); - на странице должны быть обычные ссылки
<a href>на следующую страницу, даже если JavaScript перехватывает клик; - не закрывайте страницы пагинации от индексации и не ставьте на них canonical на первую страницу — каждая страница должна быть каноничной сама для себя.
Курсорная пагинация в API
Пагинация через OFFSET проста, но на больших таблицах становится медленнее, а при добавлении новых записей элементы дублируются или пропускаются. Курсор решает обе проблемы: клиент передаёт идентификатор последнего полученного элемента.
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
API возвращает список и nextCursor — значения последнего элемента, закодированные в строку. Если nextCursor пустой, данных больше нет. Для сортировки нужен индекс по (created_at, id).
Курсор не позволяет прыгнуть сразу на страницу 37, поэтому для классической пагинации с номерами страниц чаще оставляют OFFSET, а курсор используют для ленты и «Показать ещё».
Подгрузка через Intersection Observer
Не слушайте событие scroll — используйте Intersection Observer и невидимый элемент-«датчик» в конце списка.
const sentinel = document.querySelector("#sentinel");
let cursor = null;
let loading = false;
const io = new IntersectionObserver(async ([entry]) => {
if (!entry.isIntersecting || loading) return;
loading = true;
const res = await fetch(`/api/posts?cursor=${encodeURIComponent(cursor ?? "")}`);
const { items, nextCursor } = await res.json();
renderItems(items);
cursor = nextCursor;
loading = false;
if (!nextCursor) io.disconnect();
}, { rootMargin: "400px" });
io.observe(sentinel);
rootMargin начинает загрузку заранее, чтобы пользователь не упирался в пустоту. Для «Показать ещё» тот же код вызывается по клику на кнопку.
Сохранение позиции при возврате назад
Самая раздражающая проблема: пользователь открыл товар, нажал «Назад» — и снова видит первую страницу. Что помогает:
- Обновляйте URL через
history.replaceStateпо мере подгрузки (?page=3), чтобы после возврата знать, сколько грузить. - Кэшируйте загруженные данные в
sessionStorageили в кэше запросов (TanStack Query, SWR) и восстанавливайте список до того, как браузер попытается вернуть прокрутку. - Сохраняйте позицию прокрутки перед уходом и возвращайте её после отрисовки списка.
- В SPA-фреймворках проверьте, как работает их встроенное восстановление прокрутки — часто оно справляется само, если данные уже в кэше.
Производительность длинных списков
- После нескольких сотен карточек включайте виртуализацию — в DOM остаются только видимые элементы.
- Задавайте размеры изображениям и используйте
loading="lazy". - Показывайте скелетоны на время загрузки, чтобы не прыгал макет.