Design a Social Feed
HardDesigning a social feed (like Twitter, LinkedIn, or Instagram) is a classic frontend system design question. It tests your ability to handle infinite scrolling, real-time updates, optimistic interactions (like/comment), media rendering, and virtualization for thousands of items while maintaining a smooth 60fps scrolling experience.
Interactive Visualization
Key Points
- Virtual scrolling is essential: only render items visible in the viewport plus a buffer
- Feed items have variable heights, requiring dynamic measurement and position caching
- New posts can be prepended without disrupting the current scroll position
- Optimistic updates for likes/comments make interactions feel instant
- Media (images, videos) needs lazy loading with aspect ratio preservation to prevent CLS
- Intersection Observer triggers both lazy loading and scroll-position-based pagination
- Feed state separates the item list (IDs) from item data (normalized entities)
- Skeleton screens improve perceived performance during initial load and pagination
Code Examples
Virtualized Feed with Dynamic Heights
// Virtual list: only renders visible items interface VirtualItem { id: string index: number offsetTop: number height: number } function useVirtualFeed( items: FeedItem[], containerRef: RefObject<HTMLElement>, estimatedItemHeight = 300 ) { const [scrollTop, setScrollTop] = useState(0) const [containerHeight, setContainerHeight] = useState(0) const heightCache = useRef<Map<string, number>>(new Map()) // Calculate positions from cached heights const virtualItems = useMemo(() => { let offset = 0 return items.map((item, index) => { const height = heightCache.current.get(item.id) ?? estimatedItemHeight const virtualItem: VirtualItem = { id: item.id, index, offsetTop: offset, height, } offset += height return virtualItem }) }, [items, estimatedItemHeight]) const totalHeight = virtualItems.length > 0 ? virtualItems[virtualItems.length - 1].offsetTop + virtualItems[virtualItems.length - 1].height : 0 // Only render items in viewport + buffer const overscan = 3 const visibleItems = virtualItems.filter(item => { const itemBottom = item.offsetTop + item.height const viewTop = scrollTop - overscan * estimatedItemHeight const viewBottom = scrollTop + containerHeight + overscan * estimatedItemHeight return itemBottom > viewTop && item.offsetTop < viewBottom }) // Measure actual heights after render const measureItem = useCallback((id: string, height: number) => { if (heightCache.current.get(id) !== height) { heightCache.current.set(id, height) } }, []) return { visibleItems, totalHeight, measureItem } }
Virtual scrolling keeps the DOM node count constant regardless of feed length. Dynamic height measurement with a cache handles variable-height items like posts with images, text, and embedded content.
New Post Insertion Without Scroll Jump
// Insert new posts at top without disrupting scroll position function useFeedUpdates(ws: WebSocketManager) { const [items, setItems] = useState<FeedItem[]>([]) const [pendingCount, setPendingCount] = useState(0) const pendingItems = useRef<FeedItem[]>([]) const scrollRef = useRef<HTMLElement>(null) useEffect(() => { return ws.subscribe('new-post', (data) => { const post = data as FeedItem // If user is scrolled down, batch new posts const isAtTop = (scrollRef.current?.scrollTop ?? 0) < 100 if (isAtTop) { // User is at top: insert immediately setItems(prev => [post, ...prev]) } else { // User is reading: show "N new posts" banner pendingItems.current = [post, ...pendingItems.current] setPendingCount(prev => prev + 1) } }) }, [ws]) const showNewPosts = useCallback(() => { setItems(prev => [...pendingItems.current, ...prev]) pendingItems.current = [] setPendingCount(0) scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' }) }, []) return { items, pendingCount, showNewPosts, scrollRef } } // UI: "3 new posts" banner function FeedHeader({ pendingCount, onShowNew }: FeedHeaderProps) { if (pendingCount === 0) return null return ( <button onClick={onShowNew} className="sticky top-0 z-10 w-full bg-blue-500 text-white py-2" > {pendingCount} new {pendingCount === 1 ? 'post' : 'posts'} </button> ) }
Inserting content above the viewport causes a scroll jump. The solution: if the user is at the top, insert immediately. Otherwise, queue new items behind a clickable banner that the user activates when ready.
Feed Architecture Overview
// Complete feed architecture // // ┌─────────────────────────────────────────┐ // │ Feed Shell │ // │ ┌───────────────────────────────────┐ │ // │ │ NewPostsBanner (sticky) │ │ // │ ├───────────────────────────────────┤ │ // │ │ VirtualizedList │ │ // │ │ ┌─────────────────────────────┐ │ │ // │ │ │ FeedItem │ │ │ // │ │ │ ├── AuthorHeader │ │ │ // │ │ │ ├── PostContent │ │ │ // │ │ │ ├── MediaGallery (lazy) │ │ │ // │ │ │ ├── EngagementBar │ │ │ // │ │ │ └── CommentPreview │ │ │ // │ │ └─────────────────────────────┘ │ │ // │ │ ┌─────────────────────────────┐ │ │ // │ │ │ FeedItem │ │ │ // │ │ │ └── ... │ │ │ // │ │ └─────────────────────────────┘ │ │ // │ │ LoadMoreSentinel (observer) │ │ │ // │ └───────────────────────────────────┘ │ // └─────────────────────────────────────────┘ // State architecture interface FeedState { // Normalized entities posts: Record<string, Post> users: Record<string, User> comments: Record<string, Comment> // Ordered list of visible post IDs feedOrder: string[] // Pagination cursor: string | null hasMore: boolean // Real-time pendingPostIds: string[] // UI expandedComments: Set<string> } // Key architectural decisions: // 1. Normalized state: update user avatar in one place // 2. Virtual list: handle 10K+ items without DOM bloat // 3. Cursor pagination: no offset drift on active feeds // 4. Optimistic likes: instant UI, rollback on failure // 5. Image lazy loading: Intersection Observer + blur placeholder // 6. WebSocket for new posts, polling fallback every 30s
The complete feed architecture combines normalized state, virtual scrolling, cursor pagination, real-time updates, and lazy-loaded media into a coherent system that scales to millions of users.
Common Mistakes
- Rendering all feed items in the DOM instead of virtualizing, causing jank at 1000+ items
- Inserting new posts at the top while the user is reading, causing disorienting scroll jumps
- Not preserving scroll position when navigating to a detail view and back
- Loading full-resolution images instead of responsive srcset with blur placeholders
- Using a flat array for feed state instead of normalized entities, causing stale data
Interview Tips
- Start by sketching the component tree: Shell > VirtualList > FeedItem > sub-components
- Discuss the scroll position preservation problem when inserting new items
- Mention virtual scrolling and dynamic height measurement as key technical challenges
- Explain the normalized state model and why entity deduplication matters for feeds
- Address real-time: WebSocket for new posts, optimistic updates for interactions