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 экранов плюс запас, остальное выгружаем, а при возврате наверх подтягиваем заново. Пользователь этого не замечает, а потребление памяти перестаёт расти с длиной сессии.
Что делать, если данные на сервере изменились между загрузками?
Список, собранный из разных запросов, может содержать устаревшие записи. Мы помечаем страницы временем загрузки и обновляем видимое окно при возврате на экран, а удалённые записи убираем по идентификатору. Так лента остаётся целостной без полной перезагрузки.
Эти сценарии мы закрываем на этапе проектирования ленты, а не после жалоб в поддержку — разбираем, как это устроено в вашем приложении.
Обсудим задачу и вернёмся с оценкой и планом
Чтобы предложить по делу, а не «по прайсу», нам нужно понять вашу текущую ленту: как она устроена сейчас, где уже проседает, какие сценарии для вас критичны. На первом разговоре разбираем не абстрактные пожелания, а конкретику — что развёрнуто, чем меряется, что болит.
После этого возвращаемся с оценкой работ и планом внедрения: что делаем в первую очередь, что можно отложить, где риски. Ниже — что стоит держать под рукой к обсуждению.
- Как сейчас устроена подгрузка: страницы, курсор, размер порции — и что происходит на медленной сети.
- Какие сценарии критичны: лента товаров, чат, соцфид, поиск — у каждого свои требования к подгрузке.
- Что происходит при сбое: пустой экран, дубли карточек, потеря позиции при возврате — какие из этих симптомов уже видели.
- Есть ли кэш и офлайн-режим, как ведёт себя приложение при обрыве связи.
- Как меряется качество: скорость первой порции, доля догрузок, отказы на длинной прокрутке.
- Что уже пробовали и почему не устроило — чтобы не повторять пройденное.
- Ограничения по срокам и релизному циклу: когда нужен результат и что нельзя ломать в текущей версии.
Опишите задачу в двух словах — какое приложение, какая лента, что беспокоит. Вернёмся с оценкой и планом, без общих слов.
| Подход к подгрузке | Как работает | Где уместен | Что даёт клиенту |
|---|---|---|---|
| Страничная пагинация | Лента запрашивается порциями с фиксированным номером страницы | Каталоги с предсказуемым порядком, разделы с фильтрами | Простая логика, понятный возврат на экран |
| Курсорная пагинация | Следующая порция берётся относительно последней видимой записи | Бесконечные ленты, новостные потоки, маркетплейсы | Нет дублей и пропусков при обновлении данных |
| Оконный просмотр | Приложение держит в памяти только видимую область списка | Длинные ленты с тяжёлыми карточками | Плавная прокрутка, стабильная работа на слабых устройствах |
| Гибридный вариант | Курсор для основной ленты, страницы для фильтров и поиска | Крупные приложения со смешанными сценариями | Предсказуемость там, где нужна, и скорость там, где важна |
| Этап внедрения | Что происходит | Кто участвует | Типовой срок |
|---|---|---|---|
| Аудит текущей ленты | Разбор сценариев прокрутки, поиск дублей и обрывов | Аналитик, ведущий разработчик | 3–5 дней |
| Проектирование схемы состояний | Описание загрузки, ошибки, пустого экрана, возврата | Разработчик, проектировщик взаимодействия | 4–7 дней |
| Реализация подгрузки | Курсор или окно просмотра, обработка границ списка | Команда мобильной разработки | 1–3 недели |
| Тестирование на реальных сценариях | Быстрая прокрутка, слабая сеть, возврат на экран | Тестировщик, разработчик | 4–6 дней |
| Передача команде | Регламенты, схемы, разбор типовых сбоев | Ведущий разработчик, поддержка | 2–3 дня |
| Слой решения | Что закрывает | На что влияет у клиента |
|---|---|---|
| Слой данных | Запросы порций, курсоры, кэш ответов | Лента не дублирует карточки при возврате |
| Слой состояний | Загрузка, ошибка, конец списка, повтор | Понятные экраны вместо «пустоты» |
| Слой интерфейса | Подгрузка у границы списка, индикаторы | Прокрутка ощущается непрерывной |
| Слой наблюдаемости | Сбои подгрузки, долгие ответы, отказы | Проблемы видны до жалоб пользователей |
Работы ведём на профессиональном стеке мобильной разработки — клиент получает предсказуемый результат на всём парке устройств.







