Пагинация: offset, cursor, infinite scroll
↑ 8.4 Frontend System Design · ← Предыдущая · → Следующая
Зачем это на собесе
Один из самых частых блоков в дизайне лент и таблиц.
Способы
| Способ | Запрос | Плюсы | Минусы |
|---|---|---|---|
| Offset/limit | ?offset=40&limit=20 (или page) | произвольный переход на страницу, просто | дубли и пропуски при вставках, медленно на больших offset (БД), нужен total |
| Cursor (keyset) | ?after=<cursor>&limit=20 | стабильно при изменениях, быстро на больших данных | нельзя прыгнуть на страницу N |
| Time-based / id-based | ?since=ts | инкрементальная загрузка | привязка к порядку |
Cursor
Ответ содержит nextCursor; курсор — непрозрачная строка (закодированные значения сортировки + id).
{ "items": [...], "nextCursor": "eyJpZCI6MTIzfQ", "hasMore": true }Клиентские паттерны
- Страницы (номера): таблицы админки, нужен переход по номеру;
- «Показать ещё»: явное действие, хороший контроль;
- Infinite scroll:
IntersectionObserverна sentinel, подгрузка при приближении к концу; - Виртуализация при больших массивах в DOM;
- Двунаправленная (чат): подгрузка вверх с сохранением позиции.
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['feed'],
queryFn: ({ pageParam }) => api.feed({ cursor: pageParam }),
initialPageParam: undefined as string | undefined,
getNextPageParam: (last) => last.nextCursor,
})Нюансы infinite scroll
- потеря позиции при возврате (восстановление скролла, кэш);
- доступ к футеру и поиск по странице;
- доступность: клавиатура, объявление загрузки;
- дедупликация элементов, если данные сдвинулись;
- индикаторы: loading, пусто, ошибка с повтором;
- SEO: страницы с URL для индексации.
Как выбрать
| Сценарий | Выбор |
|---|---|
| Таблица с переходом по номеру, стабильные данные | offset |
| Лента, чат, активные вставки | cursor |
| Мобильный список | infinite scroll + cursor |
| Отчёт с фильтрами | серверные фильтры + offset/cursor |
Вопросы с ответами
Почему cursor лучше offset на активных данных?
Offset «плывёт» при вставках и удалениях (дубли и пропуски), cursor привязан к последнему элементу и стабилен; ещё быстрее на больших таблицах.
Как не потерять позицию при возврате на список?
Хранить кэш страниц (Query), восстановить скролл (scrollRestoration, сохранение offset), либо хранить страницу и фильтры в URL.