Запит списку з 10 000 товарів одним викликом — класична помилка, яку наші інженери бачать у кожному третьому проєкті. API повертає 40 МБ JSON, додаток зависає на парсингу, користувач бачить білий екран на 8 секунд і йде. Ми впроваджуємо пагінацію на рівні контракту між клієнтом і сервером: це забезпечує зниження навантаження API в 3–5 разів і дає плавну навігацію без затримок. Оцініть API вашого проєкту — зв'яжіться з нами для аудиту.
Реалізація пагінації даних: offset чи cursor?
Більшість розробників беруть offset-пагінацію за замовчуванням — ?page=2&limit=20. Це працює, поки дані статичні. Варто додати живу стрічку, де записи вставляються на початок, і користувач на сторінці 3 пропустить записи або побачить дублі: INSERT на початок зсуває всі offset'и. На таблицях з 500 000+ рядків offset сканує до 100 000 рядків через індексацію B-tree — запит виконується за 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-пагінація краще за offset у 3 рази за швидкістю на таблицях від 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 в'юмоделі. Для впровадження SwiftUI пагінації з кешуванням даних offline використовуйте комбінацію з CoreData та URLSession.
Кеш та 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 — довге очікування.







