Розробка ленти підписок (Following Feed) в мобільній програмі
Лента підписок — технічно найскладніший компонент соціальної програми. Не тому що написати SELECT posts WHERE author_id IN (following_ids) ORDER BY created_at DESC складно — це працює до ~10K користувачів. Проблема починається коли потрібно тримати ленту актуальною в реальному часі, обробляти «знаменитостей» з мільйоном підписчиків у feed, і відправляти перший екран швидко.
Fan-out vs Fan-in: вибір архітектури
Два класичних підходи до формування ленти:
| Fan-out on write (push) | Fan-in on read (pull) | |
|---|---|---|
| Принцип | При публікації пишемо в ленти всіх підписчиків | При запиті ленти збираємо з підписок |
| Плюси | Читання швидке (готова лента в Redis) | Немає дублювання даних, простіше для «зірок» |
| Мінуси | Запис дорога для популярних авторів | Читання повільніше, складніше ранжування |
| Коли | До ~100K підписчиків у автора | Автори з мільйонами фолловерів |
Більшість програм використовує гібрид: fan-out для звичайних користувачів, fan-in для «зірок» (>50K підписчиків). Порог настоюється.
Для 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).
Realtime-обновлення
Три варіанти:
Pull to refresh — користувач тягне вниз, запитуємо пости новіші за firstPost.created_at. Найпростіший варіант, працює везде.
WebSocket/SSE — сервер пушить нові пости клієнту. При отриманні показуємо баннер «N нових постів» вгорі ленти (як Twitter). Клієнт не вставляє їх автоматично — тільки по тапу на баннер, інакше лента прискочує під пальцем.
Long polling — компроміс без WebSocket.
На iOS WebSocket — URLSessionWebSocketTask. На Android — OkHttp WebSocket. На Flutter — web_socket_channel.
Пагінація та cursor
Обов'язково cursor-based, не OFFSET:
-
cursor=created_atостаннього поста на поточній сторінці (ISO 8601 string) - Запит:
GET /feed?cursor=2024-11-15T10:30:00Z&limit=20 - Відповідь:
{ items: [...], next_cursor: "...", has_more: true }
При OFFSET на 100-й сторінці база вичитує 2000 рядків тільки щоб пропустити. При великій кількості підписок та постів — секунди очікування.
Кеширование на клієнті
iOS — зберігайте перші 50-100 постів ленти у CoreData або Realm. При відкритті програми — миттєво покажіть кеш, одночасно запитайте нові пости. Коли нові пості прийшли — тихо вставте їх на початок (або покажіть баннер). NSFetchedResultsController + NSDiffableDataSourceSnapshot для гладкого оновлення без мерцання.
Android — Room + Paging 3 з RemoteMediator. Локальна база — джерело істини, RemoteMediator підгружає дані з мережі у Room, Paging 3 рендерить з Room.
Flutter — Hive або Isar для локального кешу, flutter_bloc для управління станом сторінок.
Алгоритмічна лента
Хронологічна лента — базис. Якщо потрібна алгоритмічна (ранжування за engagement): зберігайте score за кожний пост, перераховуйте через worker (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-пагінацією → UI ленти з кешем → realtime-обновлення → нагрузкове тестування (k6) на сценарій «1000 запитів ленти одночасно».
Часові рамки
Базова лента з pull-to-refresh та пагінацією — 2-3 дні. З realtime WebSocket, кешем, алгоритмічним ранжуванням — 7-10 днів. Вартість розраховується індивідуально.







