JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeQuestionssystem-design
Prev

Learn the concept

Data Layer Architecture

system-design
mid
feed

Design a social media news feed with infinite scroll

infinite-scroll
news-feed
virtualization
pagination
cursor-pagination
intersection-observer
optimistic-updates
lazy-loading
performance
accessibility
system-design
Quick Answer

A social media feed requires cursor-based pagination for data fetching, list virtualization to render only visible items, Intersection Observer for infinite scroll triggers, optimistic updates for interactions (likes/comments), and careful memory management to prevent performance degradation during long scroll sessions.

Detailed Explanation

The news feed is the second most frequently asked frontend system design question after autocomplete. It tests your understanding of data fetching, rendering performance, user interactions, and resource management at scale.

Requirements

Functional Requirements:

  • Infinite scroll loading more posts as the user scrolls down
  • Mixed content types (text, images, videos, shared posts)
  • Interactions: like, comment, share with optimistic updates
  • Pull-to-refresh on mobile
  • New post indicator ("5 new posts") without jarring scroll position changes
  • Skeleton loading states during fetch

Non-Functional Requirements:

  • Smooth 60fps scrolling even with 1000+ posts loaded
  • First meaningful paint under 1.5 seconds
  • Memory usage stays bounded during long sessions
  • Works on low-end mobile devices (2GB RAM)
  • Accessible: screen readers can navigate the feed, new content is announced

Data Fetching: Cursor-Based Pagination

Why cursor-based instead of offset-based?

Offset pagination (?page=2&limit=20) breaks when new posts are added:

  • User is on page 2 (posts 21-40)
  • A new post is added at the top
  • Page 3 request returns post 40 again (shifted by 1) — duplicate!

Cursor pagination uses a stable pointer:

GET /api/feed?cursor=post_abc123&limit=20

Response:

JSON
{
  "posts": [...],
  "nextCursor": "post_xyz789",
  "hasMore": true
}

The cursor is typically an encoded timestamp + post ID for deterministic ordering. It is stable regardless of insertions or deletions.

Infinite Scroll with Intersection Observer

Instead of listening to scroll events (which fire 60+ times per second and cause jank), use Intersection Observer:

  1. Place a sentinel <div> at the bottom of the feed
  2. When it enters the viewport, trigger the next page fetch
  3. Show a loading skeleton while fetching
  4. Append new posts to the list
  5. Move the sentinel below the new posts

This is more performant than scroll listeners because:

  • Intersection Observer is asynchronous and runs off the main thread
  • No throttling/debouncing needed
  • Works correctly with nested scroll containers

Virtualization: Only Render Visible Items

This is the most critical performance optimization. Without virtualization, a feed with 500 posts creates 500+ DOM nodes, causing:

  • High memory usage (each DOM node ~ 1-2KB)
  • Slow reflows and repaints
  • Jank during scrolling

Virtualization renders only the items visible in the viewport plus a small overscan buffer (e.g., 5 items above and below). As the user scrolls, items are recycled.

Libraries:

  • react-window: Lightweight (3KB), fixed or variable height items
  • @tanstack/virtual: Framework-agnostic, dynamic heights, horizontal/grid layouts
  • react-virtuoso: Best for variable-height content like social feeds, has built-in grouping

For a social feed with variable-height posts (text-only vs image vs video), use dynamic height virtualization — measure each item after render and cache the measurement.

Optimistic Updates

When a user clicks "Like":

  1. Immediately update the UI (increment count, fill heart icon)
  2. Send the API request in the background
  3. If the request fails, roll back the UI and show an error toast

This makes the app feel instant. The key implementation details:

  • Store the previous state before optimistic update (for rollback)
  • Use a unique request ID to handle race conditions
  • Queue rapid interactions (double-tap like/unlike) and only send the final state

Image and Video Optimization

Lazy loading images:

  • Use loading="lazy" on <img> elements (native browser support)
  • For finer control, use Intersection Observer to load images when they are within 1-2 viewport heights
  • Serve responsive images with srcset for different screen densities
  • Use blur-up placeholders (tiny base64 image → full image) for perceived performance

Video handling:

  • Never autoplay videos off-screen — waste of bandwidth and battery
  • Use Intersection Observer: play when > 50% visible, pause when scrolled away
  • Load video poster/thumbnail first, load the video player only on interaction
  • On mobile, prefer HLS/DASH adaptive streaming

Skeleton Loading States

Show skeleton placeholders that match the layout of real posts:

  • Animated pulse effect for visual feedback
  • Match the actual content layout (avatar circle, text lines, image rectangle)
  • Show 3-5 skeletons for the initial load, 2-3 for subsequent pages
  • Transition smoothly from skeleton to content (no layout shift)

Pull-to-Refresh (Mobile)

  1. Detect overscroll at the top of the feed (touch events or overscroll CSS)
  2. Show a refresh indicator
  3. Fetch the latest posts
  4. Prepend new posts to the top of the list
  5. Maintain scroll position

New Post Indicator

When new posts arrive via real-time (SSE/WebSocket):

  • Do NOT prepend them automatically (causes jarring scroll jumps)
  • Show a floating pill: "5 new posts" at the top
  • On click, scroll to top and prepend the new posts
  • Include the new post count in the indicator using aria-live="polite" for screen readers

Memory Management

During long scroll sessions (user scrolls through 500+ posts):

  • Virtualization keeps DOM nodes bounded (~20-30 nodes)
  • Unmount media for off-screen posts: revoke blob URLs, pause videos, remove image sources
  • Limit total posts in memory: Keep the last 200 posts; if the user scrolls back up past the limit, re-fetch
  • Clear image decode caches for far-off-screen images

Accessibility

  • Feed container has role="feed" with aria-label="News Feed"
  • Each post has role="article" with aria-labelledby pointing to the author name
  • Announce new content loads: aria-live="polite" region saying "20 more posts loaded"
  • Provide a "Skip to next post" keyboard shortcut (j/k like Gmail)
  • Focus management: after loading more posts, keep focus position stable
  • Like/comment buttons have clear aria-label with current state: "Like post by Alice, currently liked"

Code Examples

Intersection Observer hook for infinite scroll with cursor paginationTypeScript
import { useEffect, useRef, useCallback, useState } from 'react';

interface Post {
  id: string;
  author: string;
  content: string;
  imageUrl?: string;
  likes: number;
  isLiked: boolean;
  createdAt: string;
}

interface FeedPage {
  posts: Post[];
  nextCursor: string | null;
  hasMore: boolean;
}

export function useInfiniteFeed() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [cursor, setCursor] = useState<string | null>(null);
  const [hasMore, setHasMore] = useState(true);
  const [isLoading, setIsLoading] = useState(false);
  const sentinelRef = useRef<HTMLDivElement>(null);
  const observerRef = useRef<IntersectionObserver | null>(null);

  const fetchNextPage = useCallback(async () => {
    if (isLoading || !hasMore) return;
    setIsLoading(true);

    try {
      const url = cursor
        ? `/api/feed?cursor=${cursor}&limit=20`
        : '/api/feed?limit=20';

      const res = await fetch(url);
      const data: FeedPage = await res.json();

      setPosts((prev) => [...prev, ...data.posts]);
      setCursor(data.nextCursor);
      setHasMore(data.hasMore);
    } catch (error) {
      console.error('Failed to fetch feed:', error);
    } finally {
      setIsLoading(false);
    }
  }, [cursor, hasMore, isLoading]);

  // Set up Intersection Observer on the sentinel element
  useEffect(() => {
    // Disconnect previous observer
    observerRef.current?.disconnect();

    observerRef.current = new IntersectionObserver(
      (entries) => {
        // When sentinel is visible, load next page
        if (entries[0]?.isIntersecting) {
          fetchNextPage();
        }
      },
      {
        // Start loading when sentinel is 200px from viewport
        rootMargin: '200px',
        threshold: 0,
      }
    );

    if (sentinelRef.current) {
      observerRef.current.observe(sentinelRef.current);
    }

    return () => observerRef.current?.disconnect();
  }, [fetchNextPage]);

  // Optimistic like toggle
  const toggleLike = useCallback(async (postId: string) => {
    setPosts((prev) =>
      prev.map((post) =>
        post.id === postId
          ? {
              ...post,
              isLiked: !post.isLiked,
              likes: post.isLiked ? post.likes - 1 : post.likes + 1,
            }
          : post
      )
    );

    try {
      await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
    } catch {
      // Rollback on failure
      setPosts((prev) =>
        prev.map((post) =>
          post.id === postId
            ? {
                ...post,
                isLiked: !post.isLiked,
                likes: post.isLiked ? post.likes - 1 : post.likes + 1,
              }
            : post
        )
      );
    }
  }, []);

  return { posts, isLoading, hasMore, sentinelRef, toggleLike };
}

Real-World Applications

Use Cases

Social Media Timeline

Facebook, Twitter/X, Instagram, and LinkedIn all use infinite scroll feeds with cursor-based pagination, virtualization, and optimistic interactions. Each has unique challenges like mixed media types, retweets/shares, and algorithm-ranked content.

E-commerce Product Listings

Amazon and Etsy use infinite scroll or load-more patterns for search results, with lazy-loaded product images, skeleton placeholders, and cursor-based pagination to handle millions of products.

Content Aggregators and News Apps

Reddit, Hacker News, and Google News use feed patterns with mixed content types, collapsible comments, and real-time score updates, requiring careful memory management during long browsing sessions.

Mini Projects

Image Gallery with Infinite Scroll

intermediate

Build a Pinterest-style masonry image gallery using the Unsplash API with cursor pagination, Intersection Observer for infinite scroll, lazy-loaded images with blur-up placeholders, and virtualized rendering.

Social Feed with Real-Time Updates

advanced

Create a full social media feed with cursor-based pagination, virtualized rendering, optimistic like/comment interactions, a 'new posts available' indicator, pull-to-refresh, and skeleton loading states.

Industry Examples

Twitter/X

Uses cursor-based pagination for the timeline with virtualized rendering. Their virtual scroller handles variable-height tweets with images, videos, quote tweets, and thread indicators while maintaining smooth 60fps scrolling

Instagram

Implements aggressive image lazy loading and video autoplay based on visibility. Their feed uses a custom virtual scroller optimized for mixed media content with placeholder shimmer animations

Meta (Facebook)

Pioneered many feed optimization techniques including relay-based data fetching, streaming server rendering for feed items, and sophisticated memory management that unmounts off-screen components during long scroll sessions

Resources

MDN - Intersection Observer API

docs

react-window - Virtualized List Library

docs

WAI-ARIA Feed Pattern

docs

Related Questions

What is the difference between client state and server state?

junior
data-layer

What are the trade-offs between client-side and server-side rendering?

junior
rendering

Design an autocomplete/typeahead search component

mid
autocomplete
Previous
How do you design a component library / design system for multiple teams?
Prev