New: Master Frontend System Design! Read our brand new Meta-Guide and solve the Autocomplete practice challenge.Read Master Guide
FrontendPrep
system-designMedium

Design an Infinite Scroll Feed

Loading...

Master the frontend system design of an infinite scroll feed. Learn about cursor vs offset pagination, DOM virtualization, IntersectionObserver, and scroll restoration.

Arvind M
Arvind MLinkedIn

Frontend System Design Master Guide

This question is part of our comprehensive Frontend System Design Series. Master the complete framework, requirements, and edge cases before diving in.

Read Guide

Design an Infinite Scroll Feed

Infinite scroll is a standard system design question for frontend engineers. It seems simple on the surface: listen for scroll events, fetch the next page of items, and append them to the DOM. However, it requires deep knowledge of layout performance, DOM containment, network efficiency, and state management to build a solution that works smoothly with thousands of dynamic items.


1. What the Interviewer Is Testing

When an interviewer asks you this question, they are looking for production-grade engineering decisions rather than just "getting it to work." Specifically, they evaluate:

  • Pagination Strategy (Cursor vs Offset): Can you explain why standard SQL-style offset-based pagination fails for real-time dynamic feeds, and why cursor-based pagination is preferred?
  • DOM Performance at Scale: How do you avoid browser lag or crash when rendering 10,000+ items with complex media (like images or autoplaying video cards)?
  • Network & Prefetching Efficiency: How do you decide when to fetch the next batch? Do you wait for the user to hit the absolute bottom, or do you prefetch ahead? How do you prevent duplicate requests?
  • Scroll Performance (Jank Prevention): Can you implement layout monitoring without triggering layout thrashing or overloading the main thread? (IntersectionObserver vs scroll listeners).

2. How to Open Your Answer (The First 2 Minutes)

Do not jump straight into code. Start by gathering requirements and clarifying scope. This immediately signals seniority.

Say something like:

*"Before I design the component, I'd like to clarify a few requirements. First, is this a live chronological feed (like Twitter or Instagram) where new posts are constantly injected at the top, or is it a search results/e-commerce catalog page? I'll assume it is a dynamic, live feed since it highlights pagination challenges.

Second, what does a feed item look like? Are we rendering video cards or text? Crucially, are the item heights fixed (e.g., exactly 320px) or variable (depending on user-generated text and media aspect ratios)? I will start with a fixed-height architecture to simplify virtualization math, and then discuss how we handle dynamic height measurements using ResizeObserver.

Finally, are we designing for desktop, mobile web, or both? Mobile viewports face tighter memory constraints and virtual keyboard overlays, which means DOM containment and virtualization will be even more critical."*


3. The 6-Step Answer Framework

Systematically present your solution using this architectural framework:

Step 1: Requirements Gathering (Functional & Non-Functional)

  • Functional Requirements:
    • Smooth, continuous scrolling of items.
    • Visual indicators for loading states (skeletons) and the end of the feed.
    • Error handling with a "Retry" mechanism if a page fetch fails.
    • Preserve the user's scroll position when they click into a post and hit the "Back" button (scroll restoration).
  • Non-Functional Requirements:
    • 60 FPS Smooth Scrolling: Capping runtime calculations to prevent main-thread jank.
    • DOM Containment: Keep the number of DOM nodes constant (capped around 100 max) regardless of how far the user scrolls.
    • Network Optimization: Throttle/debounce fetch requests, prefetch next pages early, and use correct page batch sizes (e.g., 20 items per page).

Step 2: Component & UI Architecture

Deconstruct the feed container into high-level React components:

Component & UI Architecture: Infinite Scroll Feed

  • Feed Wrapper: Manages the local query state, active page tokens, and fetch actions.
  • Virtual Window: Coordinates which indices should be rendered based on scroll offsets.
  • Intersection Anchor (Sentinel): A zero-height empty element positioned right below the list. When it crosses into the viewport boundary, it triggers the loading of the next page.
  • State Model:
    • items: Map or array of fetched feed posts.
    • cursor: String representing the pointer for the next API request.
    • isLoading: Spinner/skeleton toggle.
    • isError: Handles failed fetch scenarios.
    • hasMore: Tells us when the database is exhausted.

Step 3: State Management & Caching

  • Global/Persistent Cache: To support back-navigation scroll restoration, do not store feed items in standard unmounted component local state. Use a persistent global store (e.g. Redux, Zustand, or a custom React Context cache keyed by the feed path).
  • Scroll Position Store: Track the scroll offset (window.scrollY or the wrapper's scrollTop) on scroll or route changes. When mounting:
    1. Restore items from the persistent cache instead of re-fetching.
    2. Run a window.scrollTo({ top: cachedScrollTop }) or offset adjustments after items render.

Step 4: Data Fetching & API Contract

Never use offset-based pagination (LIMIT 20 OFFSET 100) for a live feed. If new items are added at the top of the database while the user is reading, the indexes shift. The user will see duplicate items on the next page. Instead, use Cursor-Based Pagination:

  • Request Contract:
    GET /api/posts?limit=20&cursor=eyJjcmVhdGVkX2F0IjoxNzE5MjM5NX0%3D
  • Response Contract:
    {
      "data": [
        { "id": "99", "title": "Understanding the Event Loop", "mediaUrl": "..." }
      ],
      "nextCursor": "eyJjcmVhdGVkX2F0IjoxNzE5MjQwMH0%3D",
      "hasMore": true
    }

Step 5: Performance Considerations

  • Intersection Observer: Instead of listening to scroll events which fire dozens of times per second (forcing main thread layouts and layout thrashing), we register the sentinel container with IntersectionObserver. The observer notifies the JS thread asynchronously when the boundary is reached.
  • DOM Virtualization (Windowing): When the user scrolls to item 500, we do not want 500 cards in the DOM. The browser will lag. We only render the active viewport items (e.g., items 10-20) plus a small padding buffer above and below.
  • Prefetch Threshold: Place the Sentinel node slightly higher up (or use the IntersectionObserver's rootMargin of 0px 0px 800px 0px) to start fetching the next page when the user is still 800px (roughly 1.5 screen heights) away from the bottom.

Step 6: Accessibility & Edge Cases

  • Skeleton States: Render skeletons of match-sized items. This prevents sudden layout shifts (CLS) when images/videos load.
  • Error Retry Boundary: If a page fetch fails, catch the error, stop the infinite observer, and show a "Failed to load posts. Retry" button. Clicking retry resumes the sentinel and triggers the page request.
  • Screen Reader Announcements: Use live regions (aria-live="polite") to announce when next pages are loading or if the end of the feed has been reached.

4. What Candidates Miss

To stand out as a senior or staff candidate, you must explicitly call out these common omissions:

  1. Using Scroll Event Listeners: Using window.addEventListener('scroll', ...) shows a lack of familiarity with modern browser API optimizations. It triggers layout calculations on every scroll frame unless heavily debounced or throttled, leading to scroll stutter (jank).
  2. Omitting Virtualization: If you just append nodes to the DOM indefinitely, your application's memory consumption grows linearly. Virtualization is mandatory for high-performance infinite scrolls.
  3. Ignoring Scroll Restoration: This is the worst UX failure. A user scrolls down 10 pages of a feed, clicks a post, views it, hits back, and is returned to the top of page 1. Mentioning this and planning state preservation shows deep empathy for UX and product quality.

5. Code Implementation

Here is how you write a clean, modern implementation of the scroll observer hook and a lightweight virtual list concept.

1. Intersection Observer Sentinel Hook

A reusable React hook that monitors a sentinel reference and triggers a callback when it enters the viewport:

import { useEffect, useRef } from 'react';
 
interface UseInfiniteScrollProps {
  isLoading: boolean;
  hasMore: boolean;
  onLoadMore: () => void;
  rootMargin?: string;
}
 
export function useInfiniteScroll({
  isLoading,
  hasMore,
  onLoadMore,
  rootMargin = '0px 0px 400px 0px' // Prefetch 400px before reaching the bottom
}: UseInfiniteScrollProps) {
  const sentinelRef = useRef<HTMLDivElement | null>(null);
 
  useEffect(() => {
    const sentinel = sentinelRef.current;
    if (!sentinel || !hasMore) return;
 
    // Create the observer
    const observer = new IntersectionObserver(
      (entries) => {
        const firstEntry = entries[0];
        if (firstEntry.isIntersecting && !isLoading) {
          onLoadMore();
        }
      },
      { rootMargin }
    );
 
    observer.observe(sentinel);
 
    // Clean up
    return () => {
      if (sentinel) {
        observer.unobserve(sentinel);
      }
    };
  }, [isLoading, hasMore, onLoadMore, rootMargin]);
 
  return sentinelRef;
}

2. DOM Virtualization Concept

Here is a simplified React implementation demonstrating virtualization for fixed-height items. We calculate the visible window indices based on the scroll position and offset the container using margins or absolute translations:

import React, { useState, useEffect, useRef } from 'react';
 
interface VirtualListProps<T> {
  items: T[];
  itemHeight: number; // e.g. 150px
  viewportHeight: number; // e.g. 600px
  renderItem: (item: T, index: number) => React.ReactNode;
}
 
export function VirtualList({
  items,
  itemHeight,
  viewportHeight,
  renderItem
}: VirtualListProps<any>) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef<HTMLDivElement | null>(null);
 
  const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(event.currentTarget.scrollTop);
  };
 
  const totalHeight = items.length * itemHeight;
 
  // 1. Calculate which items are visible
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 2); // Buffer 2 items above
  const endIndex = Math.min(
    items.length - 1,
    Math.floor((scrollTop + viewportHeight) / itemHeight) + 2 // Buffer 2 items below
  );
 
  const visibleItems = items.slice(startIndex, endIndex + 1);
 
  // 2. Position the visible window correctly using transform
  const offsetY = startIndex * itemHeight;
 
  return (
    <div
      ref={containerRef}
      onScroll={handleScroll}
      style={{
        height: viewportHeight,
        overflowY: 'auto',
        position: 'relative',
        width: '100%',
        border: '1px solid #333'
      }}
    >
      {/* 3. The scroll runway structure */}
      <div style={{ height: totalHeight, width: '100%', position: 'relative' }}>
        <div
          style={{
            transform: `translateY(${offsetY}px)`,
            left: 0,
            right: 0,
            position: 'absolute'
          }}
        >
          {visibleItems.map((item, idx) => {
            const actualIndex = startIndex + idx;
            return renderItem(item, actualIndex);
          })}
        </div>
      </div>
    </div>
  );
}

6. Summary & Key Takeaways

  • IntersectionObserver over Scroll Listeners: Offload intersection math to the browser's optimized pipeline. Keep JS handlers thin and clean.
  • Virtualize Your List: Capping the active nodes in the DOM tree prevents layout/memory bloat and ensures long-term rendering reliability.
  • Favor Cursor Pagination: Avoid double rendering and sequence shifting bugs inherent to SQL offsets in real-time chronological layouts.
  • Address UX and State Restoration: Senior engineers build architectures that preserve active items and scroll offsets when users navigate back and forth.

Finished practicing this challenge?

Mark it as completed to track your progress, or bookmark it to review later.

Loading...

Share this Resource

Help other developers level up by sharing this study guide.

⚡ Weekly newsletter

Crack Your Next Frontend Interview.

Join senior engineers who receive practical, deep-dive frontend challenges, detailed concepts, and blueprints directly in their inbox.

  • Senior level React, JS, and CSS interview blueprints
  • System Design & performance optimization deep-dives
  • 100% free, zero spam, unsubscribe with one click

Join the Study Track

We value your privacy. Unsubscribe at any time.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.