The Question
FE DesignScalable Infinite Scroll Feed System
Design a highly performant infinite scroll feed similar to modern social media platforms (e.g., Facebook, LinkedIn). The system must handle an unbounded number of items while maintaining a 60fps scroll rate. Key areas of focus include DOM virtualization strategies, efficient data fetching using Intersection Observer, memory management to prevent browser crashes, scroll position restoration, and handling variable-height content dynamically. Explain your approach to state normalization and how you ensure accessibility in a feed that constantly updates.
React
TypeScript
Intersection Observer API
TanStack Query
Virtualization
ResizeObserver
Zustand
Tailwind CSS
Questions & Insights
Clarifying Questions
Content Type & Layout: Is the feed a standard single-column list (e.g., LinkedIn/Instagram) or a multi-column masonry layout (e.g., Pinterest)?
Assumption: We will design for a single-column, variable-height list as the MVP.
Media Density: Does the feed contain heavy media like auto-playing videos or high-resolution images?
Assumption: The feed includes text and images; video-specific optimizations (like intersection-based play/pause) are out of scope for the core MVP but noted.
Scale & Performance: How many items should the user be able to scroll through in one session?
Assumption: Support for 1,000+ items without UI degradation using DOM virtualization.
Data Source: Is the data strictly sequential (cursor-based) or can users jump to specific pages?
Assumption: Cursor-based pagination is used to prevent duplicate items during live updates.
Crash Strategy
The Trigger Mechanism: How do we detect the "end of list" efficiently? Use the Intersection Observer API on a "Sentinel" element to trigger the next fetch, avoiding expensive scroll event listeners.
The Memory Bottleneck: How do we handle thousands of DOM nodes? Implement Windowing/Virtualization to ensure only the visible items (plus a small buffer) are rendered in the DOM.
The Data Flow: How do we manage the growing list of data? Use a Normalized Client-Side Cache to store fetched items, ensuring that updating a "Like" count on item #5 reflects across the UI without re-fetching the whole list.
UX Continuity: How do we handle the "Back Button" problem? Persist scroll position and fetched data in the global state/cache so the user returns to the exact spot they left.
Elite Bonus Points
Image Pre-fetching & Blurhash: Use low-resolution placeholders (Blurhash) and pre-fetch images for the next "page" to eliminate white flashes during rapid scrolling.
Dynamic Height Handling: Use
ResizeObserver within virtual rows to recalculate offsets when images or dynamic content load asynchronously.Adaptive Loading: Detect user connection speed (Network Information API) to serve lower-quality images or skip auto-playing videos on slow 3G.
Scroll Restoration Engine: Implement a custom hook that saves scroll offsets to
sessionStorage to handle browser crashes or accidental navigations.Design Breakdown
Requirements
Functional Requirements:
Infinite scroll: Auto-fetch next page when the user nears the bottom.
Feed Item display: Support text, images, and basic interactions (Like/Share).
Pull-to-refresh: Allow users to manually trigger a refresh for new content.
Loading/Error states: Show clear indicators at the bottom of the list.
Non-Functional Requirements:
Performance: Maintain 60 FPS scrolling; TBT (Total Blocking Time) < 100ms.
Scalability: Handle 10,000+ items in memory using virtualization.
Accessibility: Use
aria-busy for loading states and role="feed".Responsiveness: Fluid layout across mobile, tablet, and desktop.
Design Summary
Concise Summary: A virtualized list architecture that leverages Intersection Observer for just-in-time data fetching and a normalized cache for state management.
Major Components:
VirtualScrollContainer: Manages the visible window of elements and absolute positioning logic.
IntersectionSentinel: A transparent element at the list footer that triggers API calls when it enters the viewport.
FeedCacheStore: A centralized store (e.g., TanStack Query) that appends paginated results and maintains item integrity.
FeedItemRenderer: A memoized component that maps domain data to the visual Post UI.
CUJ Walkthrough:
User scrolls -> VirtualScrollContainer calculates visible range.
User nears bottom -> IntersectionSentinel triggers
fetchNextPage().FeedCacheStore executes API call -> Appends results -> VirtualScrollContainer updates total height.
Simplicity Audit: This architecture avoids complex "Bi-directional" scrolling (scrolling up for history) and stick to a "Forward-only" infinite scroll for the MVP, reducing state complexity.
Architecture Decision Rationale:
Why this architecture?: Virtualization is non-negotiable for "infinite" lists to prevent browser crashes. Intersection Observer is significantly more performant than "onScroll" handlers which fire on every pixel.
Requirement Satisfaction: Meets performance goals via virtualization, functional goals via cursor-based fetching, and UX goals via scroll restoration.
System Diagram
Architecture Deep Dive
Presentation Layer
Component Hierarchy: The
AppShell provides the navigation; the FeedPage hosts the VirtualScrollContainer. This container manages the viewport and renders FeedItemRenderer instances only for items currently in view.Interaction Layer:
FeedItemRenderer handles local interactions like "Likes." We use delegated event listeners if the list is extremely large. Input validation on comments is handled locally before bubbling to the Application layer.Rendering Layer: We use Virtualization (Windowing). Only ~10 items are kept in the DOM. As the user scrolls, we swap the data in the existing DOM nodes or unmount/mount nodes with absolute positioning to maintain the scrollbar height.
UI Frameworks / Tools: React for UI, Tailwind for styling, and
framer-motion for smooth layout transitions when items are deleted or added.Application Layer
Data Fetching Layer: Using TanStack Query (React Query). It handles the
fetchNextPage logic, deduplication of requests, and provides isFetchingNextPage status. We use cursor-based pagination (e.g., ?cursor=timestamp_id) to ensure stability.State Management Layer: The list of posts is stored in a Flat/Normalized Store. This prevents the "zombie child" problem where an update to a post in one part of the app doesn't reflect in the feed.
Routing & Navigation: URL tracks the feed category (e.g.,
/feed/trending). Scroll position is saved in sessionStorage on navigation away and restored on mount.Domain Layer
Business Rules: Logic for filtering blocked users, hiding sensitive content, or inserting "Sponsored" posts every N items is encapsulated here, independent of the UI.
Entities / Models: The
Post entity ensures data integrity (e.g., ensuring createdAt is a valid Date object) regardless of the raw JSON returned by the API.Inter-layer Contracts: Interfaces define the
FeedResponse (data array + nextCursor), allowing us to swap the API (e.g., REST to GraphQL) without touching the UI components.Infrastructure Layer
API / Network: Axios or Fetch with an interceptor for Auth headers. We implement a Retry Strategy with exponential backoff for failed pagination requests.
Storage: Use
localStorage for persisting the last 20 feed items for "Instant-on" loading during the next session, improving the perceived performance (LCP).Wrap Up
Wrap-up
Evaluation: The design balances extreme performance (virtualization) with developer productivity (React Query).
Trade-offs: Virtualization makes "Find in page" (Cmd+F) difficult as most content isn't in the DOM. This can be mitigated by providing a custom search feature.
Optimization: For a Staff-level implementation, we would implement Dynamic Prefetching: if the user scrolls faster, we increase the fetch buffer; if they slow down, we prioritize high-quality image loading for the visible items.