Infinite scroll & feed UI concept: cursor-based pagination vs offset, virtualization (TanStack Virtual / react-window), IntersectionObserver load-more, scroll restoration, optimistic updates with rollback, WebSocket new-posts pill. Architecture: Browser (viewport, virtualizer, TanStack Query cache, IntersectionObserver, History API) + Network (Image CDN, edge cache with stale-while-revalidate) + Backend (Feed API, ranking service, Posts DB with composite index, WebSocket gateway). 5 scenarios: cursor pagination + IO load-more, virtualization for 10K items, scroll restore on back nav, optimistic like + error retry banner, WS realtime new-posts pill with backpressure. 2 ADRs: cursor vs offset pagination; infinite scroll vs load-more vs numbered pagination (dark-pattern discussion).
Лента кажется простым списком карточек, пока в ней не появляются реальные масштабы: тысячи элементов, новые посты сверху, картинки, лайки, комментарии, переходы в detail page и возврат назад, слабые телефоны, плохая сеть и ranking, который меняется на сервере. Без правильной архитектуры frontend быстро получает дубликаты страниц, пропущенные элементы, 10000 DOM nodes, layout shift от изображений, потерянную scroll position и re-render всего списка после одного лайка.
Infinite scroll - один из самых частых frontend system design кейсов, потому что в нем сходятся API contract, browser performance, state management, кеширование, UX и этика продукта. Twitter/X, Instagram, TikTok, GitHub Activity, Slack messages, Notion database и маркетплейсы используют разные варианты одного набора проблем: как стабильно получать следующую порцию, как держать DOM маленьким, как не дергать пользователя при realtime updates и как не превратить endless feed в dark pattern.
Знание этой темы помогает проектировать не только social feed. Те же паттерны нужны для audit logs, search results, catalog pages, issue trackers, chat history, large tables и notification centers.
Feed - это поток упорядоченных элементов. Backend должен дать стабильный cursor, frontend должен хранить pages и cursor state, virtualizer должен рендерить только видимое окно, image layer должен не ломать layout, а UX должен уважать scroll context пользователя.
Три принципа держат систему устойчивой. Cursor pagination вместо offset для динамической ленты. Virtualization/windowing вместо рендера всего массива. Explicit cache model вместо случайного локального state: server cache, optimistic updates, invalidation, stale-while-revalidate, scroll restoration.
Важно разделять ordering и rendering. Ranking service решает, какие посты и в каком порядке вернуть. API упаковывает это в cursor protocol. React Query/SWR хранит страницы. Virtualizer показывает 20-100 DOM nodes. Viewport и IntersectionObserver запускают prefetch. Каждый слой решает свою задачу.
Диаграмма показывает Browser, Network и Backend. В Browser есть visible viewport, virtualizer, TanStack Query cache, IntersectionObserver и History API для scroll restoration. Network содержит Image CDN и edge cache. Backend содержит Feed API, ranking service, Posts DB с composite index и WebSocket gateway для новых элементов.
Поток initial load идет от viewport к cache: cache miss, запрос первой страницы, ranking service читает Posts DB, API возвращает items и nextCursor. Virtualizer отрисовывает только видимые карточки и небольшой overscan buffer. Когда sentinel около низа списка попадает в viewport, IntersectionObserver запускает prefetch следующей страницы.
Отдельный путь идет для изображений: карточка задает width/height или aspect-ratio, браузер лениво грузит image через CDN, placeholder предотвращает CLS. Еще один путь - realtime: WebSocket сообщает о новых постах, но UI не вставляет их автоматически в текущий viewport. Вместо этого показывает "N new posts" pill, чтобы пользователь сам решил, когда сбросить context.
Первый сценарий: cursor pagination и load-more через IntersectionObserver. Пользователь видит первые 20 элементов. API возвращает nextCursor, который может быть opaque token, composite key (created_at, id) или ranking token. Когда bottom sentinel близко, frontend запрашивает следующую страницу с cursor. Урок: cursor должен быть стабильным относительно сортировки, иначе insert/delete сверху создадут дубли и пропуски.
Второй сценарий: virtualization для 10000 items. Без windowing браузер держит тысячи DOM nodes, layout и paint дорожают, mobile начинает лагать. Virtualizer хранит общую высоту списка, измеряет visible range, рендерит только окно и overscan. Для variable-height карточек нужен measurement через ResizeObserver или библиотека вроде react-virtuoso/TanStack Virtual. Урок: большой список - это не большой DOM.
Третий сценарий: scroll restoration. Пользователь открыл пост, нажал Back и ожидает вернуться в ту же позицию, с теми же загруженными pages. Если cache уничтожен или scroll восстановлен до загрузки высот, пользователь попадает в начало. Нужны сохраненные pages, stable item keys, estimated sizes, restoration после hydration и аккуратная работа с browser history. Связанная тема: ::concept{slug="state-management"}.
Четвертый сценарий: optimistic like. UI увеличивает счетчик сразу, mutation уходит на API. Если сеть упала, cache делает rollback или показывает retry state. Урок: optimistic update должен быть локальным к item, не должен re-render всю ленту, и обязан иметь план для reject. key={index} здесь особенно опасен: при reorder React может применить состояние к чужой карточке.
Пятый сценарий: realtime new-posts pill. WebSocket говорит, что пришли новые элементы. Автоматическая вставка сверху меняет scroll offset и пользователь теряет место чтения. Правильный UI складывает новые элементы в queue, показывает pill, а после клика вставляет их и прокручивает к началу. Для очень активных лент нужен backpressure: не держать бесконечную очередь в памяти, а агрегировать "many new posts" и делать refresh.
Cursor vs offset pagination. Offset (LIMIT 20 OFFSET 100) удобен для numbered pages и admin tables, но плохо работает в динамической ленте. Новый пост сверху сдвигает offset, следующая страница может повторить уже виденный элемент или пропустить другой. Большой offset также дорог для БД. Cursor/keyset pagination использует индекс и stable sort key, поэтому лучше для feed. Минус: нельзя дешево прыгнуть на page 100, а cursor надо проектировать как контракт. Для feed это нормальный компромисс: пользователь скроллит поток, а не открывает произвольный номер страницы.
Infinite scroll vs Load more vs numbered pagination. Infinite scroll минимизирует friction и хорошо подходит для discovery/feed, но может быть dark pattern: пользователь теряет ощущение конца, footer недоступен, сложно вернуться к конкретному месту. Load more дает больше контроля и проще для accessibility. Numbered pagination подходит для поиска, отчетов, таблиц и SEO-страниц, где важна адресуемость. Практичный выбор: social feed - infinite с сохранением позиции; issue list/admin - numbered или cursor plus explicit next; commerce/search - часто pagination или Load more, чтобы пользователь мог сравнивать и возвращаться.
Virtualization has cost. Она усложняет dynamic height, focus management, browser find-in-page, screen reader чтение длинного списка и anchor links. Для 100 элементов virtualization не нужна. Для 1000+ карточек или тяжелого контента она критична. Нужно тестировать keyboard navigation: focus не должен исчезать, когда элемент выходит из virtual window. Для accessibility иногда лучше иметь Load more pages вместо бесконечного virtualized canvas-like списка.
React Query/SWR cache vs custom state. Готовые server-state библиотеки дают staleTime, cache keys, invalidation, optimistic update hooks, retries и pagination helpers. Custom store может быть оправдан для очень специфичной ленты, но чаще приводит к ручным race conditions: две страницы загружаются параллельно, cursor перепутан, rollback перетирает свежие данные.
Twitter/X использует queue for new tweets, ranking и разные timelines. Instagram и TikTok особенно зависят от image/video loading, prefetch, CDN и keeping viewport smooth. GitHub Issues чаще использует explicit pagination/load more, потому что пользователю важны фильтры, адресуемость и контроль. Slack chat history - reverse infinite scroll: старые сообщения грузятся вверх, а scroll position должна оставаться стабильной.
Marketplace и search pages часто выбирают pagination не потому, что не умеют infinite scroll, а потому что пользователь сравнивает результаты, возвращается назад, открывает несколько tabs и ожидает воспроизводимый порядок. Audit logs используют cursor/keyset, потому что данные append-only и важны пропуски.
Для CloudArch эта тема связана с ::concept{slug="web-perf-core-vitals"}: изображения без размеров дают CLS, тяжелый список портит INP, плохой skeleton ухудшает perceived loading. Связана и с ::concept{slug="state-management"}: feed cache, mutations и restoration должны иметь явную модель.
Offset pagination на динамической ленте - источник дубликатов и пропусков. key={index} в React ломает состояние при reorder. Рендер 10000 nodes без virtualization убивает scroll performance. Re-render всего списка при изменении лайка показывает, что item memoization, stable props или store selectors не продуманы.
Изображения без width/height или aspect-ratio вызывают layout shift. Eager loading всех изображений тратит bandwidth и ухудшает LCP. Spinner вместо skeleton создает пустой экран и дергает layout. IntersectionObserver без guard может отправить несколько одинаковых запросов, если sentinel быстро входит/выходит из viewport.
Realtime auto-insert сверху - UX-дефект: пользователь читает один пост, список сдвигается. Отсутствие scroll restoration после detail page убивает навигацию. Непрозрачные cursors, которые на самом деле являются offset, ломаются при insert. Отсутствие dedupe по item id приводит к повторяющимся карточкам после retries.
Accessibility ошибки: бесконечная лента без landmarks и heading context, отсутствие announcement для подгруженных items, focus на исчезнувшем virtualized элементе, footer, до которого невозможно добраться, кнопки без label, touch targets слишком маленькие.
Не используйте infinite scroll для задач, где пользователю нужна завершенность, сравнение и адресуемость: invoices, search results with exact count, admin tables, legal documents, transaction history, checkout steps. Там лучше numbered pagination, filters, saved views и export.
Не включайте virtualization для маленьких списков. Она добавляет сложность и может ухудшить screen reader behavior. Не используйте algorithmic feed, если продукт требует прозрачного chronological order: audit log, incident timeline, banking history. Не делайте auto-refresh, если пользователь читает или редактирует item в списке.
Не используйте feed UI как dark pattern. Если нужен footer, legal links или explicit end, infinite scroll должен иметь reachable footer, Load more control или end marker. Пользователь должен контролировать поток.
Изучите TanStack Query, SWR, TanStack Virtual, react-virtuoso, IntersectionObserver и Relay Cursor Connections spec. По performance полезны материалы web.dev про CLS, INP, lazy loading и image optimization. Внутри курса продолжайте через ::concept{slug="web-perf-core-vitals"}, ::concept{slug="state-management"} и кейсы вроде ::case{slug="twitter-feed"}, ::case{slug="instagram-feed"}, ::case{slug="slack-messages"}, если они есть в вашем наборе материалов.