Сделать ленту подписок (following feed) в мобильном приложении, работающую при 100K+ подписчиков, — нетривиальная задача. Простой SQL-запрос SELECT * FROM posts WHERE author_id IN (SELECT followee_id FROM follows WHERE follower_id = :user) ORDER BY created_at DESC ломается при масштабировании: на 1 млн подписчиков и 10 млн постов время ответа превышает 3 секунды. Добавьте realtime, необходимость мгновенного старта и популярного автора с миллионом подписчиков — и без продуманной архитектуры не обойтись. Гибридная схема fan-out для обычных и fan-in для звёзд снижает затраты на инфраструктуру на 30-50%. Наша команда с 7+ лет опыта и 50+ проектами в сфере социальных приложений гарантирует решение, которое выдержит нагрузку. Проведём нагрузочное тестирование k6 на 1000 rps — и только после этого сдаём проект.
Как выбрать архитектуру для разработки ленты подписок: fan-in или fan-out?
Два классических подхода: fan-out on write (push) и fan-in on read (pull). При fan-out публикация поста сразу записывается в ленты всех подписчиков — чтение становится быстрым, но запись дорогой для популярных авторов. Fan-in, наоборот, при запросе собирает посты из подписок — нет дублирования данных, но чтение может быть медленнее. На практике применяют гибрид: fan-out для обычных пользователей (до 100K подписчиков у автора) и fan-in для «звёзд» с миллионами фолловеров. Порог настраивается. Для MVP достаточно fan-in:
SELECT p.*, u.name, u.avatar_url FROM posts p JOIN follows f ON p.author_id = f.followee_id JOIN users u ON p.author_id = u.id WHERE f.follower_id = :user_id AND p.created_at < :cursor ORDER BY p.created_at DESC LIMIT 20; Индексы: follows(follower_id), posts(author_id, created_at DESC). С таким запросом при 1 млн подписок время ответа — 100-150 мс.
| Характеристика | Fan-out on write | Fan-in on read |
|---|---|---|
| Скорость чтения | O(1) — моментально | O(N) — может быть медленным при 1M+ подписок |
| Скорость записи для звезды | Очень дорого (миллионы копий) | O(1) — только запись поста |
| Хранение | Много дублирования (50+ копий на пост) | Минимум дублирования |
| Нагрузка на сервер | Высокая на запись, низкая на чтение | Низкая на запись, высокая на чтение |
| Масштабирование | Сложно для популярных авторов | Хорошо для любого количества подписчиков |
Cursor-based пагинация: преимущества перед offset
Cursor-пагинация обрабатывает запрос за 50 мс при таблице в 10 млн строк, тогда как OFFSET на 100-й странице — до 2 секунд. Используем cursor = created_at последнего поста (ISO 8601): GET /feed?cursor=LAST_SEEN_POST_TIMESTAMP&limit=20 Ответ: { items: [...], next_cursor: "...", has_more: true }.
Реализация простая: индекс на (created_at) обеспечивает O(log n). В отличие от OFFSET, cursor не чувствителен к вставкам — дубли не появляются.
Как обеспечить мгновенную загрузку ленты?
Кэширование на клиенте — ответ. На iOS с Swift сохраняем первые 50-100 постов ленты в CoreData или Realm. При открытии приложения — мгновенно показываем кэш, одновременно запрашиваем новые посты. Когда новые посты пришли — тихо вставляем их в начало или показываем баннер. NSFetchedResultsController + NSDiffableDataSourceSnapshot для плавного обновления без мерцания. На Android с Kotlin используем Room + Paging 3 с RemoteMediator. Локальная база — источник истины, RemoteMediator подгружает данные из сети в Room, Paging 3 рендерит из Room. На Flutter — Hive или Isar для локального кэша, flutter_bloc для управления состоянием страниц.
| Платформа | Кэширующая технология | Библиотека для изображений | Преимущества |
|---|---|---|---|
| iOS (Swift) | CoreData / Realm | Kingfisher | NSFetchedResultsController, DiffableDataSource |
| Android (Kotlin) | Room + Paging 3 | Coil | RemoteMediator, интеграция с Compose |
| Flutter (Dart) | Hive / Isar | cached_network_image | Простота, fast startup |
Кэш позволяет сократить загрузку данных при старте на 80% и снизить расход трафика.
Realtime-обновления: стратегии и реализация
- Pull to refresh — пользователь тянет вниз, запрашиваем посты новее
firstPost.created_at. Самый простой, работает везде. - WebSocket/SSE — сервер пушит новые посты клиенту. Показываем баннер «N новых постов» вверху ленты (как Twitter). Клиент не вставляет их автоматически — только по тапу на баннер, иначе лента прыгает под пальцем.
- Long polling — компромисс без WebSocket.
На iOS WebSocket — URLSessionWebSocketTask. На Android — OkHttp WebSocket. На Flutter — web_socket_channel.
Настройка realtime-обновлений через WebSocket
- Создайте WebSocket-соединение при входе в ленту.
- Сервер отправляет событие
new_postс ID поста. - Клиент отображает баннер «X новых постов».
- По тапу на баннер загружаем недостающие посты через API.
Алгоритмическая лента
Хронологическая лента — базис. Если нужна алгоритмическая (ранжирование по engagement): хранить score за каждый пост, пересчитывать через воркер (BullMQ/Celery) при добавлении лайков/комментариев. Клиент запрашивает ленту с параметром sort=ranked. Для первого запуска — хронологическая, после набора данных — переключение на алгоритмическую. Обе ленты как отдельные вкладки (Reels vs Following у Instagram).
Скролл и производительность
UICollectionView с UICollectionViewCompositionalLayout и DiffableDataSource — золотой стандарт на iOS. Prefetch данных через UICollectionViewDataSourcePrefetching. Изображения — Kingfisher с кэшированием в памяти и на диске. На Android LazyColumn (Compose) или RecyclerView с ConcatAdapter. Изображения — Coil с rememberAsyncImagePainter. Главная причина дёрганой прокрутки — декодировка изображений на main thread. Kingfisher и Coil делают это в background по умолчанию. При кастомной загрузке — DispatchQueue.global(qos: .userInitiated).async (iOS) или Dispatchers.IO (Android).
Что входит в работу
- Архитектурная схема ленты (выбор fan-in/fan-out/гибрид) под вашу нагрузку.
- API с cursor-пагинацией и realtime-событиями.
- UI ленты с кэшем и плавной прокруткой.
- Нагрузочное тестирование (k6) на сценарий «1000 запросов ленты одновременно».
- Документация, код и поддержка при деплое.
Этапы разработки ленты подписок
- Анализ нагрузки: ожидаемое количество подписчиков, TPS, размер поста.
- Выбор архитектуры: fan-in/fan-out/гибрид, порог для звёзд.
- Проектирование API: REST + WebSocket, cursor-пагинация.
- Реализация UI: коллекция/список с кэшем и prefetch.
- Realtime-интеграция: WebSocket/SSE, баннер новых постов.
- Нагрузочное тестирование: k6, 1000 rps, мониторинг.
- Деплой и мониторинг: CI/CD, логи, алерты.
Сроки
Базовая лента с pull-to-refresh и пагинацией — 2-3 дня. С realtime WebSocket, кэшем, алгоритмическим ранжированием — 7-10 дней. Стоимость рассчитывается индивидуально. Закажите разработку под ключ и получите стабильное решение для вашего социального приложения. Свяжитесь с нами для оценки проекта — гарантируем прозрачный подход и сертифицированных разработчиков. Получите консультацию инженера.







