Infinite scroll у мобільному застосунку: де ламається стрічка
Infinite scroll виглядає як простий прийом: користувач доходить до низу стрічки контенту — застосунок підвантажує наступну порцію.
На практиці нескінченне прокручування ламається в чотирьох передбачуваних місцях, і кожне з них замовник бачить як «застосунок гальмує» або «картки задвоюються».
Перший збій — дублі карток. Підвантаження даних смикається за кількома подіями одразу: скрол, оновлення списку, повернення з фону. Два запити йдуть паралельно, відповіді приходять у різному порядку, і у стрічці з'являються однакові елементи.
Лікується це не «захистом від дурня», а єдиним джерелом правди: сторінки нумеруються, відповіді склеюються за ідентифікатором, а повторний запит блокується до завершення попереднього.
Другий — втрата позиції скролу. Користувач відкриває картку, повертається — і стрічка стрибає на початок або на випадкове місце. Причина в тому, що список перезбирається з нуля, а збережена позиція не прив'язана до конкретного елемента.
Правильно зберігати якір: ідентифікатор видимої картки та зсув усередині неї, тоді повернення стає рівно туди, де користувач зупинився.
Третій — ривки під час швидкого прокручування. Поки дані вантажаться, у стрічці порожнеча або підказка завантаження, і палець проскакує повз ще не відрендерені картки.
Тут працює передзавантаження за екран і заглушки-скелетони фіксованої висоти: список не змінює геометрію, прокручування залишається плавним.
Четвертий — витоки пам'яті на довгій сесії. Година гортання — і в пам'яті висять тисячі відрендерених карток і підвислих запитів. Рішення — перевикористання елементів і скасування запитів, які вже не потрібні.
Розбираємо, як це закривається в реальній розробці.
Що дає коректна реалізація підвантаження контенту
Коректно зроблений infinite scroll — це не «стрічка, яка просто докручується», а передбачувана система підвантаження: сторінка запитується заздалегідь, до того як користувач дійде до краю, елементи не дублюються при повторних запитах, а пам'ять не зростає лінійно з глибиною прокрутки.
Різниця видна в метриках: відгук застосунку залишається рівним на сотнях екранів униз, а не деградує до третьої-четвертої сотні елементів.
Перший вимірюваний ефект — стабільність списку. Коли ключі елементів унікальні, а вставка нових сторінок відбувається без перезбирання вже відрендерених, зникають дублі та «стрибки» позиції при поверненні на екран.
Користувач, який згорнув застосунок на середині стрічки й повернувся через годину, опиняється там само, де зупинився, — частка відмов при поверненні падає, бо екран не перезавантажується з нуля.
Другий ефект — передбачуване споживання пам'яті. Стрічка утримує в пам'яті вікно навколо видимої області, а не всі завантажені елементи: відпрацьовані йдуть із дерева, кеш зображень обмежений за розміром.
На довгих сесіях це знімає зростання споживання та пов'язані з ним примусові вивантаження застосунку системою.
Третій — рівний відгук застосунку на прокрутку. Підвантаження наступної сторінки запускається із запасом, паралельно з інерцією скролу, а не в момент упору в низ; при повільній мережі показується скелетон, а не порожній екран.
Разом це дає продуктивність стрічки, яка не залежить від того, як довго користувач гортає, — і саме її ми закладаємо в реалізацію.
Варіанти підвантаження: сторінки, курсор, вікно перегляду
Є три способи віддавати дані в стрічку, і вибір між ними визначає, як застосунок поводитиметься на швидкому скролі та за живих правок контенту.
| Варіант | Коли підходить | Обмеження |
|---|---|---|
| Посторінкове завантаження зі зміщенням | Статичні каталоги, рідкісні вставки, прості списки | Під час додавання записів на початок стрічки елементи дублюються й «стрибають»: зміщення сторінок рахується від початку, а воно вже зсунулося |
| Курсорна пагінація | Живі стрічки, чати, маркетплейси з частими правками | Потрібен стабільний ключ сортування й підтримка курсора на боці сервісу; перехід на довільну сторінку неможливий |
| Вікно перегляду | Обмежений набір даних, який уже надійшов цілком | Пам'ять зростає разом з обсягом, а свіжі записи не підтягуються без повторного запиту |
Посторінкове завантаження зі зміщенням — найшвидший старт: сервер віддає зріз за номером сторінки, клієнт просто збільшує лічильник. Проблема проявляється під навантаженням: поки користувач гортає, на початок стрічки надходять нові записи, і наступна сторінка віддає вже побачене.
В інтерфейсі це дублі карток і загублені елементи — найпомітніше у стрічках із частими публікаціями.
Курсорна пагінація тримає позицію за останнім отриманим елементом, а не за його порядковим номером. Вставки й видалення вище курсора не зсувають вибірку, тому дублів немає навіть за активних правок.
Ціна — серверна логіка: потрібен стабільний ключ сортування, а перехід «одразу на середину» списку стає неочевидним.
Вікно перегляду працює інакше: дані надходять одним набором, а на екран потрапляє лише видима частина. Це знімає навантаження з сервера, але перекладає його на пам'ять пристрою — на великих обсягах застосунок починає гальмувати.
Передзавантаження наступної порції за 1-2 екрани до кінця списку згладжує очікування, але не скасовує обмежень обраної схеми.
Етапи впровадження: від аудиту стрічки до передачі команді
Впровадження infinite scroll починається не з коду, а з аудиту стрічки: дивимося, як застосунок зараз віддає дані, де кешуються сторінки, які запити йдуть під час швидкого скролу.
На цьому кроці стає видно, що саме гальмує — розмір відповіді, відсутність пагінації на бекенді або зайві перемальовування списку. Без аудиту будь-яке налаштування перетворюється на підгонку симптомів.
Послідовність робіт
- Аудит стрічки. Розбираємо поточний потік даних: скільки запитів іде на один екран, як поводиться застосунок при втраті мережі, що відбувається з пам'яттю при довгому прокручуванні. Фіксуємо вузькі місця у звіті.
- Схема завантаження. Проєктуємо контракт: розмір сторінки, курсор замість зміщення, поведінка при дубльованих записах, стратегія кешу та повторних спроб. Схема узгоджується з бекендом — інакше клієнт і сервер почнуть розходитися в очікуваннях.
- Налаштування. Реалізуємо підвантаження з випереджальним запитом за 1-2 екрани до кінця списку, індикатором завантаження та захистом від паралельних однакових запитів. Окремо закриваємо сценарії: порожня відповідь, помилка мережі, кінець стрічки.
- Тестування прокручування. Прогоняємо на реальних сценаріях — повільна мережа, різкий флік до низу, повернення з фону, поворот екрана. Перевіряємо, що позиція зберігається, а пам'ять не зростає нескінченно.
- Передача коду. Віддаємо команді замовника репозиторій зі змінами, опис контракту завантаження, інструкцію щодо правки розміру сторінки та чек-лист перевірок перед релізом.
Кожен етап закінчується артефактом: звіт аудиту, схема, робочий код, результати тестів, документація. Це дозволяє підключати вашу команду на будь-якому кроці й не залежати від одного виконавця. Опишіть задачу — повернемося з оцінкою та планом за етапами.
Артефакти: конфігурації, схеми станів, регламенти
Розробка стрічки не закінчується робочим екраном. Щоб застосунок можна було розвивати, передавати іншій команді та безпечно змінювати поведінку підвантаження, ми фіксуємо рішення в артефактах — від моделі даних до регламенту супроводу. Нижче — що саме залишається у вас на руках після здачі.
- Конфігурація стрічки — опис параметрів підвантаження: розмір сторінки, поріг спрацювання довантаження, поведінка при порожній відповіді та при вичерпанні даних. Змінюється без переписування екрана.
- Схема станів — діаграма переходів між «завантаження», «дані є», «порожньо», «помилка», «кінець списку» та правил повернення з кожного стану. За нею видно, де саме стрічка може «залипнути».
- Правила кешування — що і на який термін зберігається локально, як інвалідується при оновленні, як поводиться застосунок без мережі та при поверненні з фону.
- Модель даних — структура елемента списку, ключі пагінації, правила дедуплікації при повторному підвантаженні та злитті сторінок.
- Документація — текстовий опис логіки підвантаження, точок розширення та типових сценаріїв зміни: новий фільтр, зміна джерела, додавання типу картки.
- Регламент супроводу — що перевіряти при релізі, які метрики дивитися (частка помилок довантаження, час до першого екрана, глибина прокрутки), як відкочувати зміни стрічки.
- Схеми інтеграції — як стрічка пов'язана з аналітикою, пошуком та офлайн-режимом, щоб нові модулі не ламали пагінацію.
Ці артефакти знімають головний ризик підтримки: логіка підвантаження перестає бути знанням одного інженера. Опишіть задачу — повернемося з оцінкою та планом, включно зі складом документації під ваш процес.
Кейс: як стрічка перестала дублювати картки
Замовник прийшов із класичною скаргою: користувачі бачать одні й ті самі картки у стрічці, а під час швидкого прокручування вниз список на секунди завмирає.
Метрики підтвердили діагноз: частка дублів сягала 8% показів, відгук на швидкий свайп просідав до 400–600 мс, а пам'ять на довгій сесії зростала лінійно — застосунок доходив до 700 МБ і система починала вивантажувати процес у фоні.
Що було під капотом
Стрічка тягла сторінки за номером offset, а ключем елемента слугував індекс у масиві. Варто було даним на сервері перевпорядкуватися між запитами — і той самий товар приїжджав двічі під різними індексами, а частина позицій зникала.
Кеш тримав усі завантажені сторінки в пам'яті без витіснення, тому довга сесія перетворювалася на утечу.
Що змінили
Перейшли на курсорну пагінацію з непрозорим токеном замість номера сторінки — сервер віддає «наступний після ось цього», і вставки в середину більше не зсувають вікно.
Ключем елемента зробили стабільний ідентифікатор запису, а не позицію: повторний прихід тієї самої сутності тепер схлопується на рівні сховища.
Додали вікно утримання — тримаємо в пам'яті поточний екран і по парі екранів у кожен бік, решта витісняється, а картинки вантажаться в міру наближення до межі.
Що стало
Частка дублів упала до нуля — повторні покази ловляться на вході. Відгук на швидке прокручування скоротився до 80–120 мс, бо підвантаження йде заздалегідь, за екран до краю, і не блокує кадр.
Пам'ять на довгій сесії стабілізувалася в районі 180–220 МБ і перестала зростати з часом: витіснення працює незалежно від того, скільки користувач гортає.
Окремо закрили відкат: якщо сервер поверне застарілий токен, стрічка не рветься, а тихо перезапитує вікно з останньої валідної позиції. Кейс маркетплейсу показав головне — infinite scroll ламається не на анімації, а на моделі даних під нею.
Чим курсорна пагінація відрізняється від сторінкової
Сторінкова пагінація спирається на зсув: клієнт запитує «перші 20», потім «наступні 20» — сервер відраховує записи від початку вибірки. Поки дані статичні, все передбачувано. Але стрічка жива: між запитами надходить новий пост, і все, що нижче, зсувається вниз на одну позицію.
Наступна сторінка починається вже не з того запису — користувач бачить дублікат або втрачає елемент, який так і не показався. Це не баг клієнта, а властивість самого зсуву: він не прив'язаний до конкретного запису, лише до його порядкового номера в моменті.
Курсорна пагінація замість номера сторінки передає вказівник на останній отриманий запис — його унікальний ключ і значення поля сортування. Сервер повертає все, що йде строго після цієї точки.
Вставка нових записів зверху на таку вибірку не впливає: курсор указує на конкретний елемент, а не на позицію. Користувач гортає стрічку без дублікатів і пропусків — це і є консистентність даних при читанні.
Вартість запитів теж розходиться. При зсуві база відраховує і відкидає всі попередні рядки, тому глибоке прокручування дорожчає з кожною сторінкою: сота порція читає марно тисячі записів.
Курсор працює за індексом і бере рівно потрібний діапазон — час відповіді залишається рівним на будь-якій глибині стрічки.
Складність підтримки вища у курсора, і це чесна ціна. Потрібен стабільний ключ сортування, складений вказівник при рівних значеннях, окрема логіка для «назад» і для переходу до довільної позиції.
Зате сторінкова пагінація, дешева в реалізації, обростає костилями рівно там, де стрічка оновлюється часто. Для нескінченного прокручування вибір однозначний: курсор.
Технічні сумніви: утечі, дублі, повернення на екран
Infinite scroll майже завжди ламається не на першому екрані, а на поверненні: користувач іде в картку товару, повертається — і стрічка стрибає на початок.
Причина в тому, що список перестворюється заново, а стан підвантаження і позиція скролу живуть у пам'яті екрана, який система вивантажила при відході у фон.
Лікується це збереженням стану стрічки окремо від самого екрана: зміщення, завантажені сторінки і ознаку «дані ще є» кладемо в модель, що переживає перестворення, і при поверненні відновлюємо позицію до відрисовки, а не після.
Дублі записів — друга за частотою скарга, і майже завжди це гонка двох запитів. Користувач смикає стрічку вниз, спрацьовує підвантаження, він смикає ще раз — іде два запити за однією і тією ж сторінкою, і обидві відповіді дописуються в список.
Закривається це блокуванням на час активного запиту плюс дедуплікацією за стійким ідентифікатором запису, а не за індексом у масиві: індекс зсувається при вставці, ідентифікатор — ні.
Чому стрічка втрачає позицію після згортання застосунку?
Система вивантажує екран з пам'яті, щоб звільнити ресурси, і при поверненні будує його заново. Якщо позиція скролу і завантажені сторінки зберігалися тільки в самому екрані, вони губляться разом з ним.
Рішення — винести цей стан у шар, який живе довше за екран, і відновлювати позицію до першої відрисовки списку.
Як обмежити пам'ять при довгій сесії?
Тримати в пам'яті всю прокручену стрічку не можна — на довгій сесії це призводить до зростання споживання і падіння. Ми обмежуємо вікно: зберігаємо останні N екранів плюс запас, решту вивантажуємо, а при поверненні вгору підтягуємо заново. Користувач цього не помічає, а споживання пам'яті перестає зростати з довжиною сесії.
Що робити, якщо дані на сервері змінилися між завантаженнями?
Список, зібраний з різних запитів, може містити застарілі записи. Ми позначаємо сторінки часом завантаження і оновлюємо видиме вікно при поверненні на екран, а видалені записи прибираємо за ідентифікатором. Так стрічка залишається цілісною без повного перезавантаження.
Ці сценарії ми закриваємо на етапі проєктування стрічки, а не після скарг у підтримку — розбираємо, як це влаштовано у вашому застосунку.
Обговоримо задачу і повернемося з оцінкою та планом
Щоб запропонувати по суті, а не «за прайсом», нам потрібно зрозуміти вашу поточну стрічку: як вона влаштована зараз, де вже просідає, які сценарії для вас критичні. На першій розмові розбираємо не абстрактні побажання, а конкретику — що розгорнуто, чим міряється, що болить.
Після цього повертаємося з оцінкою робіт і планом впровадження: що робимо в першу чергу, що можна відкласти, де ризики. Нижче — що варто тримати під рукою до обговорення.
- Як зараз влаштована підгрузка: сторінки, курсор, розмір порції — і що відбувається на повільній мережі.
- Які сценарії критичні: стрічка товарів, чат, соцфід, пошук — у кожного свої вимоги до підгрузки.
- Що відбувається при збої: порожній екран, дублі карток, втрата позиції при поверненні — які з цих симптомів уже бачили.
- Чи є кеш і офлайн-режим, як поводиться застосунок при обриві зв'язку.
- Як міряється якість: швидкість першої порції, частка довантажень, відмови на довгому прокручуванні.
- Що вже пробували і чому не влаштувало — щоб не повторювати пройдене.
- Обмеження за строками та релізним циклом: коли потрібен результат і що не можна ламати в поточній версії.
Опишіть задачу в двох словах — який застосунок, яка стрічка, що турбує. Повернемося з оцінкою та планом, без загальних слів.







