Запрос списка из 10 000 товаров одним вызовом — классическая ошибка, которую наши инженеры видят в каждом третьем проекте. API возвращает 40 МБ JSON, приложение зависает на парсинге, пользователь видит белый экран на 8 секунд и уходит. Мы внедряем пагинацию на уровне контракта между клиентом и сервером: это снижает нагрузку в 3–5 раз и даёт плавную навигацию без задержек. Оцените API вашего проекта — свяжитесь с нами для аудита.
Реализация пагинации данных: offset или cursor?
Большинство разработчиков берут offset-пагинацию по умолчанию — ?page=2&limit=20. Это работает, пока данные статичны. Стоит добавить живую ленту, где записи вставляются в начало, и пользователь на странице 3 пропустит записи или увидит дубли: INSERT в начало сдвигает все offset'ы. На таблицах с 500 000+ строк offset сканирует до 100 000 строк, чтобы сдвинуть курсор — запрос выполняется за 200–400 мс.
Cursor-пагинация решает проблему: сервер возвращает next_cursor, клиент передаёт его в следующем запросе. Курсор — это опакный токен (обычно base64 от id + timestamp), который фиксирует позицию в наборе данных. PostgreSQL-бэкенды реализуют это через WHERE id < :cursor ORDER BY id DESC LIMIT 20. Никаких дублей, никаких пропусков. Скорость запросов остаётся стабильной — 50–100 мс независимо от размера таблицы.
| Критерий | Offset-пагинация | Cursor-пагинация |
|---|---|---|
| Простота реализации | Высокая | Средняя (требуется курсор на бэкенде) |
| Дубли/пропуски при вставке | Да | Нет |
| Производительность на больших таблицах | Ухудшается (O(N)) | Стабильная (O(1)) |
| Поддержка смещения к произвольной странице | Да (page=3) | Сложно (только последовательный скролл) |
| Кэширование | Легкое (страницы по page) | Требуется хранить курсоры |
Почему cursor-пагинация эффективнее offset?
Cursor-пагинация в 3 раза быстрее offset на таблицах от 100 000 строк — бенчмарки показывают снижение времени ответа с 300 до 90 мс. Кроме того, она гарантирует консистентность данных при параллельных вставках. Для лент новостей, сообщений в чатах и каталогов с частыми обновлениями это обязательный выбор.
Как настроить кэширование с помощью RemoteMediator?
На Android с Paging 3 курсорная пагинация реализуется через RemoteMediator + PagingSource:
class ItemPagingSource(
private val api: ItemsApi,
private val query: String
) : PagingSource<String, Item>() {
override suspend fun load(params: LoadParams<String>): LoadResult<String, Item> {
return try {
val response = api.getItems(
cursor = params.key,
limit = params.loadSize,
query = query
)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = response.nextCursor
)
} catch (e: HttpException) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<String, Item>): String? {
return state.anchorPosition?.let { anchor ->
state.closestPageToPosition(anchor)?.nextKey
}
}
}
LazyColumn в Compose подключается через collectAsLazyPagingItems() — это готовый binding, который обрабатывает состояния Loading, Error, NotLoading без лишнего кода.
На iOS аналог — compositional layout с UICollectionViewDiffableDataSource и NSDiffableDataSourceSnapshot. Prefetch реализуется через UICollectionViewDataSourcePrefetching: метод prefetchItemsAt вызывается за N ячеек до края, что позволяет запустить сетевой запрос заранее. Без prefetch при скорости скролла больше 500 px/s видно пустые ячейки — пользователь ждёт подгрузки 1–2 секунды.
Как внедрить cursor-пагинацию на Android: пошаговая инструкция
- Определите контракт API: сервер возвращает
next_cursorв JSON-ответе. - Создайте
PagingSource<String, Item>, который передаёт курсор вloadParams.key. - Подключите
RemoteMediatorдля offline-кэша:RemoteMediatorполучает данные из сети и сохраняет в Room. - В
PagingSourceреализуйтеgetRefreshKey, чтобы после обновления восстановить позицию. - Настройте
LazyColumnсcollectAsLazyPagingItems()и явныйLoadStateFooterс кнопкой retry.
Сравнение стратегий кэширования: RemoteMediator vs. простой кэш
| Параметр | RemoteMediator + Room | Простой кэш (LruCache) |
|---|---|---|
| Offline-доступ | Да | Нет |
| Консистентность при обновлениях | ETag/Last-Modified | Нет механизма |
| Сложность реализации | Средняя | Низкая |
| Экономия трафика | 60–80% | 0% |
| Рекомендация | Высоконагруженные проекты | Прототипы, статичные данные |
Бесконечный скролл и pull-to-refresh
Бесконечный скролл без состояния ошибки — типичная проблема. Сеть пропала на 5-й странице, запрос завис, пользователь скролит вниз и ничего не происходит. Нужен явный LoadStateFooter с кнопкой retry.
В Paging 3:
adapter.addLoadStateListener { loadState ->
binding.retryButton.isVisible = loadState.source.append is LoadState.Error
binding.progressBar.isVisible = loadState.source.append is LoadState.Loading
}
Pull-to-refresh сбрасывает пагинацию к первой странице через adapter.refresh() — Paging 3 инвалидирует PagingSource и начинает заново. На SwiftUI это refreshable модификатор, который вызывает invalidateQueries() в TCA или обновляет @StateObject вьюмодели.
Кэш и offline
Paging 3 + Room — стандартная связка для offline-first. RemoteMediator записывает данные в локальную БД, PagingSource читает из Room, а не из сети. Пользователь видит данные даже без интернета, свежие данные подгружаются фоном.
Ключевой момент: стратегия инвалидации кэша. Если сервер обновил запись, локальная копия устареет. Решение — ETag или Last-Modified в заголовках ответа: клиент отправляет If-None-Match, сервер возвращает 304 без тела при отсутствии изменений. Room обновляет только изменённые записи через @Insert(onConflict = OnConflictStrategy.REPLACE). Это снижает объём скачиваемых данных на 60–80%.
Что входит в работу
- Аудит текущего API и спецификация контракта пагинации.
- Реализация cursor/offset-пагинации на клиенте (Android/iOS/кроссплатформа).
- Интеграция RemoteMediator и Room для offline-режима.
- Настройка prefetch и retry-логики.
- Тестирование на реальных устройствах с имитацией плохой сети.
- Документация кода и рекомендации по доработке бэкенда.
- Поддержка после внедрения: 2 недели консультаций.
Сроки
Простая offset-пагинация без кэша — 1–2 дня. Cursor-пагинация с RemoteMediator, offline-кэшем и retry-логикой — 3–5 дней. Стоимость рассчитывается индивидуально после анализа требований и существующего API. Мы имеем 5+ лет опыта в мобильной разработке и более 30 проектов с пагинацией — доверьтесь нашим специалистам. Закажите аудит пагинации вашего приложения — мы подберем оптимальное решение.
Типичные ошибки при внедрении пагинации
- Отсутствие prefetch на iOS — пустые ячейки при быстром скролле.
- Использование offset в ленте с частыми вставками — дубли и пропуски.
- Игнорирование состояний загрузки и ошибок: пользователь не видит индикатора.
- Отсутствие кэширования — каждый скролл вызывает сетевой запрос.
- Неправильный выбор размера страницы: 5 элементов — частые подгрузки, 100 — долгое ожидание.







