Frontend Interview Questions 2025: Hooks Intricacies, RSC, Performance Tradeoffs, and System Design

R
R.S. Chauhan
10/13/2025 50 min read
Frontend Interview Questions 2025: Hooks Intricacies, RSC, Performance Tradeoffs, and System Design

Introduction

The frontend landscape in 2025 has evolved significantly, with React Server Components becoming mainstream, hooks patterns maturing, and performance optimization techniques becoming more sophisticated. This guide explores the most critical interview topics that separate senior developers from the rest.


Part 1: React Hooks Intricacies

Question 1: Explain the closure trap in useEffect and how to avoid it

The Problem:

The closure trap occurs when a useEffect hook captures stale values from previous renders because it doesn't properly declare its dependencies. This happens because JavaScript closures capture variables from their lexical scope at the time of function creation.

Example of the Problem:

function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count); // Always logs 0!
      setCount(count + 1); // Always sets to 1!
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // Empty dependency array causes closure trap
  
  return <div>{count}</div>;
}

Why It Happens:

The interval callback captures the initial count value (0) when the effect first runs. Since the dependency array is empty, the effect never re-runs, and the callback never gets updated with new count values.

Solutions:

  1. Functional Updates: Use the function form of setState
setCount(c => c + 1); // Works correctly
  1. Proper Dependencies: Include all used variables
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(timer);
}, [count]); // Re-creates interval on each count change
  1. useRef for Latest Values: When you need current values without re-running effects
const countRef = useRef(count);
countRef.current = count;

useEffect(() => {
  const timer = setInterval(() => {
    setCount(countRef.current + 1);
  }, 1000);
  return () => clearInterval(timer);
}, []);

Question 2: When would you use useMemo vs useCallback, and what are their performance implications?

useMemo: Memoizes a computed value

useCallback: Memoizes a function reference

Key Differences:

// useMemo - returns the result of the function
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

// useCallback - returns the function itself
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

// These are equivalent:
const memoizedCallback = useCallback(fn, deps);
const memoizedCallback = useMemo(() => fn, deps);

When to Use useMemo:

  1. Expensive calculations: When computing a value is CPU-intensive
  2. Referential equality: When passing objects/arrays to child components that use React.memo
  3. Dependency in other hooks: When the value is used in dependency arrays

When to Use useCallback:

  1. Passing callbacks to optimized children: Components wrapped in React.memo
  2. Dependencies in useEffect: When the function is a dependency in another hook
  3. Event handlers for expensive renders: But only when the child component is memoized

Performance Tradeoffs:

Both hooks have overhead:

  • Memory cost: Storing previous values/functions
  • Comparison cost: Checking dependencies on each render

Don't use them when:

  • The computation is cheap (faster to just recalculate)
  • Child components aren't memoized
  • The dependencies change frequently anyway

Good Example:

function ProductList({ products, filter }) {
  // Good: Expensive filtering operation
  const filteredProducts = useMemo(() => {
    return products.filter(p => 
      complexFilterLogic(p, filter)
    ).sort((a, b) => complexSort(a, b));
  }, [products, filter]);
  
  // Good: Prevents re-render of memoized child
  const handleClick = useCallback((id) => {
    selectProduct(id);
  }, [selectProduct]);
  
  return (
    <div>
      {filteredProducts.map(p => 
        <MemoizedProduct 
          key={p.id} 
          product={p}
          onClick={handleClick}
        />
      )}
    </div>
  );
}

Question 3: Explain the rules of hooks and why they exist at the implementation level

The Rules:

  1. Only call hooks at the top level (not in loops, conditions, or nested functions)
  2. Only call hooks from React function components or custom hooks

Why These Rules Exist:

React relies on the order of hook calls to maintain state between renders. React doesn't use variable names or IDs to track which state belongs to which hook call.

Internal Implementation Concept:

// Simplified React internals
let currentFiber = null;
let hookIndex = 0;

function useState(initialValue) {
  const fiber = currentFiber;
  const hooks = fiber.hooks || [];
  const hook = hooks[hookIndex] || { state: initialValue };
  
  hooks[hookIndex] = hook;
  hookIndex++;
  
  const setState = (newValue) => {
    hook.state = newValue;
    scheduleRender(fiber);
  };
  
  return [hook.state, setState];
}

What Breaks When Rules Are Violated:

function BrokenComponent({ condition }) {
  const [a, setA] = useState(1);
  
  if (condition) {
    const [b, setB] = useState(2); // ❌ Conditional hook
  }
  
  const [c, setC] = useState(3);
}

// First render (condition=true):
// hooks[0] = state for 'a'
// hooks[1] = state for 'b'
// hooks[2] = state for 'c'

// Second render (condition=false):
// hooks[0] = state for 'a'
// hooks[1] = state for 'c' (WRONG! This gets the old 'b' state)

Correct Pattern:

function CorrectComponent({ condition }) {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2); // Always call
  const [c, setC] = useState(3);
  
  // Use condition in render logic instead
  return <div>{condition ? b : null}</div>;
}

Part 2: React Server Components (RSC)

Question 4: Explain the fundamental difference between Server Components and traditional SSR

Traditional SSR (Server-Side Rendering):

  1. Server renders HTML from React components
  2. Sends HTML to browser
  3. Browser downloads JavaScript bundle
  4. React hydrates the HTML (attaches event handlers, makes it interactive)
  5. All components become client-side components after hydration

React Server Components:

  1. Server components run ONLY on the server
  2. They never ship to the client (zero JavaScript bundle impact)
  3. Can directly access databases, file systems, etc.
  4. Output is a special format (not HTML), describing the UI
  5. Client components are rendered on the client as usual

Key Differences:

Aspect SSR RSC
JavaScript bundle Full bundle sent Only client components sent
Re-rendering Client-side Can refetch from server
Data fetching useEffect or getServerSideProps Direct in component
State/Interactivity After hydration Only in Client Components

Example:

// Server Component (no 'use client' directive)
async function ProductPage({ id }) {
  // Runs on server only - direct DB access
  const product = await db.products.findById(id);
  const reviews = await db.reviews.findByProduct(id);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <ProductImage src={product.image} />
      {/* Client Component for interactivity */}
      <AddToCartButton product={product} />
      <ReviewList reviews={reviews} />
    </div>
  );
}

// Client Component
'use client'
function AddToCartButton({ product }) {
  const [added, setAdded] = useState(false);
  
  return (
    <button onClick={() => setAdded(true)}>
      {added ? 'Added!' : 'Add to Cart'}
    </button>
  );
}

Benefits:

  • Reduced JavaScript bundle size
  • Better initial load performance
  • Automatic code splitting
  • Direct backend access without API routes
  • Can use server-only libraries

Question 5: How do you handle data mutations in a Server Component architecture?

The Challenge:

Server Components can't use hooks like useState or handle events directly. You need patterns for mutations that work in this model.

Solution 1: Server Actions

// Server Component
async function TodoList() {
  const todos = await db.todos.findAll();
  
  async function addTodo(formData) {
    'use server' // Marks this as a Server Action
    
    const text = formData.get('text');
    await db.todos.create({ text });
    revalidatePath('/todos'); // Refresh this route
  }
  
  return (
    <div>
      {todos.map(todo => <div key={todo.id}>{todo.text}</div>)}
      
      <form action={addTodo}>
        <input name="text" />
        <button type="submit">Add</button>
      </form>
    </div>
  );
}

Solution 2: Client Components with API Routes

// Server Component
async function ProductPage({ id }) {
  const product = await getProduct(id);
  
  // Pass data to Client Component
  return <ProductDetails product={product} />;
}

// Client Component
'use client'
function ProductDetails({ product }) {
  const [optimisticStock, setOptimisticStock] = useState(product.stock);
  
  async function handlePurchase() {
    // Optimistic update
    setOptimisticStock(prev => prev - 1);
    
    try {
      await fetch('/api/purchase', {
        method: 'POST',
        body: JSON.stringify({ productId: product.id })
      });
      
      // Trigger server re-fetch
      router.refresh();
    } catch (error) {
      // Revert optimistic update
      setOptimisticStock(product.stock);
    }
  }
  
  return (
    <div>
      <p>Stock: {optimisticStock}</p>
      <button onClick={handlePurchase}>Buy Now</button>
    </div>
  );
}

Solution 3: Hybrid Approach with Optimistic Updates

'use client'
import { useOptimistic } from 'react';

function CommentSection({ comments, postId }) {
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (state, newComment) => [...state, { ...newComment, pending: true }]
  );
  
  async function submitComment(formData) {
    const text = formData.get('text');
    
    addOptimisticComment({ text, author: 'You' });
    
    await createComment(postId, text); // Server Action
  }
  
  return (
    <div>
      {optimisticComments.map((comment, i) => (
        <div key={i} style={{ opacity: comment.pending ? 0.5 : 1 }}>
          {comment.text}
        </div>
      ))}
      
      <form action={submitComment}>
        <input name="text" />
        <button>Submit</button>
      </form>
    </div>
  );
}

Question 6: What are the security implications of Server Components and how do you prevent data leaks?

Key Security Concerns:

  1. Accidental Data Exposure: Server Components can access sensitive data, but you must be careful what you pass to Client Components

Problem Example:

// ❌ DANGEROUS: Server Component
async function UserProfile({ userId }) {
  const user = await db.users.findById(userId);
  
  // This passes EVERYTHING to the client!
  return <ClientSideProfile user={user} />;
}

// The entire user object (including passwordHash, email, etc.)
// is serialized and sent to the browser

Safe Approach:

// ✅ SAFE: Server Component
async function UserProfile({ userId }) {
  const user = await db.users.findById(userId);
  
  // Only pass what's needed
  const publicData = {
    name: user.name,
    avatar: user.avatar,
    bio: user.bio
  };
  
  return <ClientSideProfile user={publicData} />;
}
  1. Server Action Security: Validate everything, never trust client input
// ✅ Proper validation
async function updateProfile(formData) {
  'use server'
  
  // 1. Authentication
  const session = await getSession();
  if (!session) throw new Error('Unauthorized');
  
  // 2. Authorization
  const userId = formData.get('userId');
  if (userId !== session.userId) {
    throw new Error('Forbidden');
  }
  
  // 3. Input validation
  const bio = formData.get('bio');
  if (bio.length > 500) {
    throw new Error('Bio too long');
  }
  
  // 4. Sanitization
  const sanitizedBio = sanitizeHtml(bio);
  
  await db.users.update(userId, { bio: sanitizedBio });
}
  1. Environment Variable Exposure
// ❌ WRONG: Client Component
'use client'
function Dashboard() {
  // This tries to access server-only env var
  const apiKey = process.env.SECRET_API_KEY; // undefined in browser!
}

// ✅ CORRECT: Server Component
async function Dashboard() {
  const data = await fetch('https://api.example.com', {
    headers: {
      'Authorization': process.env.SECRET_API_KEY // Only on server
    }
  });
  
  return <DataDisplay data={data} />;
}

Best Practices:

  • Always validate Server Action inputs
  • Use type-safe parsers (like Zod) for form data
  • Never pass sensitive data to Client Components
  • Implement proper authentication checks in Server Actions
  • Use environment variables prefixed with NEXT_PUBLIC_ only for truly public data

Part 3: Performance Tradeoffs

Question 7: Explain the performance implications of code splitting strategies

What is Code Splitting?

Breaking your JavaScript bundle into smaller chunks that are loaded on-demand rather than all at once.

Strategy 1: Route-Based Splitting

// Automatic with Next.js app router
app/
  products/page.js  // Separate chunk
  checkout/page.js  // Separate chunk
  profile/page.js   // Separate chunk

Pros:

  • Simple to implement
  • Reduces initial bundle size
  • Clear loading boundaries

Cons:

  • May delay rendering of routes
  • Doesn't help with large shared dependencies

Strategy 2: Component-Based Splitting

import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <Spinner />,
  ssr: false // Don't render on server
});

function Dashboard() {
  return (
    <div>
      <QuickStats />
      {/* Only loads when this component renders */}
      <HeavyChart data={data} />
    </div>
  );
}

Pros:

  • Fine-grained control
  • Can defer expensive components
  • Reduces main bundle size

Cons:

  • More complex to manage
  • Can cause layout shifts
  • Network waterfalls if not careful

Strategy 3: Vendor Splitting

// webpack config
optimization: {
  splitChunks: {
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'all'
      },
      common: {
        minChunks: 2,
        name: 'common',
        chunks: 'all'
      }
    }
  }
}

Pros:

  • Better caching (vendor code changes less)
  • Shared dependencies in one file
  • Predictable bundle structure

Cons:

  • Large vendor bundle if not careful
  • May download unused code

Performance Tradeoffs to Consider:

  1. Bundle Size vs Request Count

    • Fewer, larger bundles: Less overhead, but more unused code
    • Many small bundles: More requests, more overhead
  2. Initial Load vs Navigation Speed

    • Eager loading: Fast navigation, slow initial
    • Lazy loading: Fast initial, potential delays later
  3. Caching vs Freshness

    • Aggressive splitting: Better caching
    • Less splitting: Simpler updates

Optimal Strategy (2025):

// Combine approaches
// 1. Route-based for pages
// 2. Component-based for heavy components
// 3. Vendor splitting for libraries

// Example: Next.js app structure
app/
  layout.js           // Core layout (eager)
  page.js            // Home (eager)
  products/
    page.js          // Products route (lazy)
    [id]/
      page.js        // Product detail (lazy)

components/
  Header.js          // Shared (in main bundle)
  ProductGrid.js     // Shared (in main bundle)
  Reviews.js         // Dynamic import (lazy)
  Analytics.js       // Dynamic import (lazy, low priority)

Measurement Approach:

// Monitor with performance API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.initiatorType === 'script') {
      console.log('Script load:', entry.name, entry.duration);
    }
  }
});

observer.observe({ entryTypes: ['resource'] });

Question 8: When should you use virtual scrolling vs pagination vs infinite scroll?

Virtual Scrolling (Windowing)

Render only visible items plus a buffer, reuse DOM elements as you scroll.

import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Best for:

  • 1,000+ items available in memory
  • Fixed or predictable item sizes
  • Tables, logs, data grids
  • When you need instant access to any item

Performance:

  • DOM nodes: ~20-50 (constant)
  • Memory: All data in RAM
  • Scrolling: Smooth, instant

Tradeoffs:

  • Complex implementation
  • Accessibility challenges
  • SEO: Content not in HTML
  • Poor for dynamic heights

Pagination

Divide data into discrete pages, load one page at a time.

function PaginatedList({ totalItems, itemsPerPage = 20 }) {
  const [page, setPage] = useState(1);
  const [items, setItems] = useState([]);
  
  useEffect(() => {
    fetch(`/api/items?page=${page}&limit=${itemsPerPage}`)
      .then(r => r.json())
      .then(setItems);
  }, [page]);
  
  return (
    <div>
      {items.map(item => <ItemCard key={item.id} {...item} />)}
      
      <button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
        Previous
      </button>
      <span>Page {page}</span>
      <button onClick={() => setPage(p => p + 1)}>
        Next
      </button>
    </div>
  );
}

Best for:

  • Search results
  • E-commerce product listings
  • When users need to find specific items
  • When you need stable URLs for each page

Performance:

  • DOM nodes: Fixed per page
  • Memory: Only current page
  • Navigation: Requires page load

Tradeoffs:

  • Slower browsing experience
  • Good for SEO (each page indexable)
  • Good for accessibility
  • Users can share specific pages

Infinite Scroll

Load more items as user scrolls near the bottom.

function InfiniteScrollList() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const observerRef = useRef();
  
  const lastItemRef = useCallback(node => {
    if (observerRef.current) observerRef.current.disconnect();
    
    observerRef.current = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting && hasMore) {
        setPage(p => p + 1);
      }
    });
    
    if (node) observerRef.current.observe(node);
  }, [hasMore]);
  
  useEffect(() => {
    fetch(`/api/items?page=${page}`)
      .then(r => r.json())
      .then(newItems => {
        setItems(prev => [...prev, ...newItems]);
        setHasMore(newItems.length > 0);
      });
  }, [page]);
  
  return (
    <div>
      {items.map((item, i) => {
        if (i === items.length - 1) {
          return <div ref={lastItemRef} key={item.id}>{item.name}</div>;
        }
        return <div key={item.id}>{item.name}</div>;
      })}
      {!hasMore && <div>No more items</div>}
    </div>
  );
}

Best for:

  • Social media feeds
  • News feeds
  • Content discovery
  • Mobile applications
  • When users browse more than search

Performance:

  • DOM nodes: Grows with scroll
  • Memory: Grows with scroll (can cause issues)
  • Scrolling: Seamless experience

Tradeoffs:

  • Can't reach footer
  • Hard to return to specific position
  • Poor SEO (content loads dynamically)
  • Growing memory usage
  • Scrollbar becomes meaningless

Decision Matrix:

Scenario Best Choice Why
10,000 row data table Virtual Scrolling Need instant access, constant DOM
E-commerce catalog Pagination SEO, filtering, stable URLs
Social feed Infinite Scroll Discovery, engagement
Logs/Terminal Virtual Scrolling Performance with huge lists
Search results Pagination Users need to find things
Chat history Virtual Scrolling Need to jump to any message

Hybrid Approach (Advanced):

function HybridList() {
  const [mode, setMode] = useState('infinite'); // or 'paginated'
  
  // Switch based on data size or user preference
  useEffect(() => {
    if (totalItems > 10000) {
      setMode('virtual');
    }
  }, [totalItems]);
  
  if (mode === 'virtual') return <VirtualScrollList />;
  if (mode === 'infinite') return <InfiniteScrollList />;
  return <PaginatedList />;
}

Question 9: Explain the performance impact of React Context and when to use alternatives

How Context Works:

const ThemeContext = createContext();

function App() {
  const [theme, setTheme] = useState('dark');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <Main />
      <Footer />
    </ThemeContext.Provider>
  );
}

function Button() {
  const { theme } = useContext(ThemeContext);
  return <button className={theme}>Click me</button>;
}

The Performance Problem:

When context value changes, ALL components that use useContext re-render, even if they only use a small part of the value.

Problem Example:

function App() {
  const [theme, setTheme] = useState('dark');
  const [user, setUser] = useState(null);
  
  // ❌ This causes ALL consumers to re-render when either changes
  const value = { theme, setTheme, user, setUser };
  
  return (
    <AppContext.Provider value={value}>
      <ThemeButton />  {/* Re-renders on user change */}
      <UserProfile />  {/* Re-renders on theme change */}
    </AppContext.Provider>
  );
}

Solution 1: Split Contexts

// ✅ Separate concerns
const ThemeContext = createContext();
const UserContext = createContext();

function App() {
  const [theme, setTheme] = useState('dark');
  const [user, setUser] = useState(null);
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <ThemeButton />  {/* Only re-renders on theme change */}
        <UserProfile />  {/* Only re-renders on user change */}
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

Solution 2: Memoize Context Value

function App() {
  const [theme, setTheme] = useState('dark');
  
  // ✅ Prevent unnecessary re-renders of provider
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  
  return (
    <ThemeContext.Provider value={value}>
      <Children />
    </ThemeContext.Provider>
  );
}

Solution 3: Use Selectors (Custom Hook)

// Not preventing re-renders, but making them cheap
const AppContext = createContext();

function useTheme() {
  const context = useContext(AppContext);
  return context.theme; // Only get what you need
}

function useUser() {
  const context = useContext(AppContext);
  return context.user;
}

// Components still re-render, but can be memoized
const ThemeButton = memo(function ThemeButton() {
  const theme = useTheme();
  return <button className={theme}>Click</button>;
});

When Context Performance Becomes a Problem:

  1. Context value changes frequently (multiple times per second)
  2. Many components (50+) consume the context
  3. Components consuming context are expensive to render

Alternatives to Consider:

1. Component Composition (Best for UI state)

// Instead of context:
function App() {
  const [theme, setTheme] = useState('dark');
  
  return (
    <Layout theme={theme}>
      <Header theme={theme} onThemeChange={setTheme} />
      <Content theme={theme} />
    </Layout>
  );
}

// Use children to avoid prop drilling:
function App() {
  const [theme, setTheme] = useState('dark');
  
  return (
    <Layout>
      <Header>
        <ThemeButton theme={theme} onChange={setTheme} />
      </Header>
      <Content theme={theme} />
    </Layout>
  );
}

2. State Management Libraries (For complex state)

// Zustand - only re-renders components that use changed state
import create from 'zustand';

const useStore = create((set) => ({
  theme: 'dark',
  user: null,
  setTheme: (theme) => set({ theme }),
  setUser: (user) => set({ user })
}));

function ThemeButton() {
  // Only subscribes to theme
  const theme = useStore((state) => state.theme);
  const setTheme = useStore((state) => state.setTheme);
  return <button onClick={() => setTheme('light')}>{theme}</button>;
}

function UserProfile() {
  // Only subscribes to user
  const user = useStore((state) => state.user);
  return <div>{user?.name}</div>;
}

3. Jotai/Recoil (Atomic state)

import { atom, useAtom } from 'jotai';

const themeAtom = atom('dark');
const userAtom = atom(null);

function ThemeButton() {
  const [theme, setTheme] = useAtom(themeAtom);
  // Only re-renders when themeAtom changes
  return <button onClick={() => setTheme('light')}>{theme}</button>;
}

Performance Comparison:

Approach Re-renders Boilerplate Use Case
Context All consumers Low Simple, infrequent updates
Split Context Only relevant Medium Multiple independent values
Zustand Only subscribers Low Frequent updates, many consumers
Jotai Only atom users Medium Fine-grained reactivity
Composition Only prop recipients None Simple component trees

When Context is Fine:

// ✅ Good use of context: Infrequent changes
const AuthContext = createContext();

function App() {
  const [user, setUser] = useState(null);
  
  // User only changes on login/logout
  return (
    <AuthContext.Provider value={{ user, setUser }}>
      <AppContent />
    </AuthContext.Provider>
  );
}

// ✅ Good use of context: Theme that rarely changes
const ThemeContext = createContext();

When to Use Alternatives:

// ❌ Bad for context: High frequency updates
const MouseContext = createContext();

function App() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  
  // Updates on every mouse move!
  useEffect(() => {
    const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    return () => window.removeEventListener('mousemove', handler);
  }, []);
  
  // This will re-render EVERY consumer on EVERY mouse move
  return (
    <MouseContext.Provider value={pos}>
      <App />
    </MouseContext.Provider>
  );
}

// ✅ Better: Use Zustand or local state

Part 4: System Design for Frontends

Question 10: Design a frontend architecture for a real-time collaborative document editor (like Google Docs)

Requirements Analysis:

  1. Multiple users editing simultaneously
  2. Real-time updates (low latency)
  3. Conflict resolution
  4. Offline support
  5. History/undo functionality
  6. Scalability (large documents, many users)

High-Level Architecture:

┌─────────────┐
│   Browser   │
│             │
│  ┌───────┐  │
│  │ Editor│  │
│  └───┬───┘  │
│      │      │
│  ┌───▼───┐  │
│  │ OT/   │  │
│  │ CRDT  │  │
│  └───┬───┘  │
│      │      │
│  ┌───▼───┐  │
│  │ Local │  │
│  │ Store │  │
│  └───┬───┘  │
└──────┼──────┘
       │ WebSocket
       │
┌──────▼──────┐
│  WebSocket  │
│   Gateway   │
└──────┬──────┘
       │
┌──────▼──────┐
│   Message   │
│   Broker    │
│  (Redis)    │
└──────┬──────┘
       │
┌──────▼──────┐
│  Document   │
│   Service   │
└──────┬──────┘
       │
┌──────▼──────┐
│  Database   │
│ (PostgreSQL)│
└─────────────┘

Component 1: Editor Layer (Frontend)

// Document editor component
function CollaborativeEditor({ documentId }) {
  const [doc, setDoc] = useState(null);
  const [users, setUsers] = useState([]);
  const editorRef = useRef(null);
  const wsRef = useRef(null);
  
  useEffect(() => {
    // Initialize CRDT document
    const yDoc = new Y.Doc();
    const yText = yDoc.getText('content');
    
    // Connect to WebSocket
    const ws = new WebSocket(`wss://api.example.com/doc/${documentId}`);
    wsRef.current = ws;
    
    // Sync provider for real-time updates
    const provider = new WebsocketProvider(
      'wss://api.example.com/doc',
      documentId,
      yDoc
    );
    
    // Handle incoming changes
    yText.observe(event => {
      // Update editor with remote changes
      applyRemoteChanges(event.changes);
    });
    
    // Handle local changes
    editorRef.current.on('change', (delta) => {
      // Convert editor delta to CRDT operations
      applyLocalChanges(yText, delta);
    });
    
    // Awareness for cursor positions
    provider.awareness.on('change', changes => {
      const states = Array.from(provider.awareness.getStates().values());
      setUsers(states);
    });
    
    return () => {
      provider.destroy();
      ws.close();
    };
  }, [documentId]);
  
  return (
    <div className="editor-container">
      <Toolbar />
      <UserCursors users={users} />
      <Editor ref={editorRef} />
    </div>
  );
}

Component 2: Conflict Resolution (CRDT Implementation)

// Using Yjs CRDT for conflict-free replicated data
class DocumentState {
  constructor() {
    this.yDoc = new Y.Doc();
    this.yText = this.yDoc.getText('content');
    this.undoManager = new Y.UndoManager(this.yText);
  }
  
  // Apply local operation
  insert(index, text) {
    this.yText.insert(index, text);
    // CRDT automatically handles conflicts
    // Each character has a unique ID (clientID + clock)
  }
  
  delete(index, length) {
    this.yText.delete(index, length);
  }
  
  // Get current state
  toString() {
    return this.yText.toString();
  }
  
  // Sync with remote
  applyUpdate(update) {
    Y.applyUpdate(this.yDoc, update);
  }
  
  // Get updates to send
  getUpdate() {
    return Y.encodeStateAsUpdate(this.yDoc);
  }
  
  undo() {
    this.undoManager.undo();
  }
  
  redo() {
    this.undoManager.redo();
  }
}

Alternative: Operational Transform (OT)

// OT requires server authority but has smaller payloads
class OTEngine {
  constructor() {
    this.revision = 0;
    this.pendingOps = [];
  }
  
  // Transform operation against concurrent operations
  transform(op1, op2) {
    if (op1.type === 'insert' && op2.type === 'insert') {
      if (op1.position < op2.position) {
        return op2; // No change needed
      } else if (op1.position > op2.position) {
        return { ...op2, position: op2.position + op1.text.length };
      } else {
        // Same position - use client ID to break tie
        return op1.clientId < op2.clientId ? op2 : 
          { ...op2, position: op2.position + op1.text.length };
      }
    }
    // Handle other operation type combinations...
  }
  
  // Apply operation with transformation
  applyOperation(op) {
    // Transform against pending operations
    let transformedOp = op;
    for (const pending of this.pendingOps) {
      transformedOp = this.transform(transformedOp, pending);
    }
    
    // Apply to document
    this.applyToDocument(transformedOp);
    
    return transformedOp;
  }
}

Component 3: Real-time Sync Layer

// WebSocket manager with reconnection
class DocumentSync {
  constructor(documentId, onUpdate) {
    this.documentId = documentId;
    this.onUpdate = onUpdate;
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectDelay = 30000;
    this.pendingOperations = [];
    
    this.connect();
  }
  
  connect() {
    this.ws = new WebSocket(
      `wss://api.example.com/doc/${this.documentId}`
    );
    
    this.ws.onopen = () => {
      console.log('Connected');
      this.reconnectAttempts = 0;
      
      // Send any pending operations
      this.flushPendingOperations();
      
      // Request current state
      this.ws.send(JSON.stringify({
        type: 'sync-request',
        revision: this.currentRevision
      }));
    };
    
    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      
      switch (message.type) {
        case 'update':
          this.handleUpdate(message);
          break;
        case 'sync-response':
          this.handleSyncResponse(message);
          break;
        case 'acknowledgment':
          this.handleAck(message);
          break;
      }
    };
    
    this.ws.onclose = () => {
      console.log('Disconnected');
      this.reconnect();
    };
    
    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
  }
  
  reconnect() {
    const delay = Math.min(
      1000 * Math.pow(2, this.reconnectAttempts),
      this.maxReconnectDelay
    );
    
    this.reconnectAttempts++;
    
    setTimeout(() => {
      this.connect();
    }, delay);
  }
  
  sendOperation(operation) {
    const message = {
      type: 'operation',
      documentId: this.documentId,
      operation,
      clientId: this.clientId,
      timestamp: Date.now()
    };
    
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message));
    } else {
      // Queue for later
      this.pendingOperations.push(message);
    }
  }
  
  flushPendingOperations() {
    while (this.pendingOperations.length > 0) {
      const op = this.pendingOperations.shift();
      this.ws.send(JSON.stringify(op));
    }
  }
  
  handleUpdate(message) {
    this.onUpdate(message.operation);
  }
}

Component 4: Offline Support

// IndexedDB for offline storage
class OfflineStorage {
  constructor(documentId) {
    this.documentId = documentId;
    this.db = null;
    this.init();
  }
  
  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('CollabEditor', 1);
      
      request.orupgrade = (event) => {
        const db = event.target.result;
        
        // Store for document content
        if (!db.objectStoreNames.contains('documents')) {
          db.createObjectStore('documents', { keyPath: 'id' });
        }
        
        // Store for pending operations
        if (!db.objectStoreNames.contains('pendingOps')) {
          const store = db.createObjectStore('pendingOps', { 
            keyPath: 'id', 
            autoIncrement: true 
          });
          store.createIndex('documentId', 'documentId', { unique: false });
          store.createIndex('timestamp', 'timestamp', { unique: false });
        }
      };
      
      request.onsuccess = (event) => {
        this.db = event.target.result;
        resolve();
      };
      
      request.onerror = reject;
    });
  }
  
  // Save document state
  async saveDocument(content) {
    const tx = this.db.transaction(['documents'], 'readwrite');
    const store = tx.objectStore('documents');
    
    await store.put({
      id: this.documentId,
      content,
      lastModified: Date.now()
    });
  }
  
  // Load document from cache
  async loadDocument() {
    const tx = this.db.transaction(['documents'], 'readonly');
    const store = tx.objectStore('documents');
    
    return new Promise((resolve, reject) => {
      const request = store.get(this.documentId);
      request.onsuccess = () => resolve(request.result);
      request.onerror = reject;
    });
  }
  
  // Queue operation for later sync
  async queueOperation(operation) {
    const tx = this.db.transaction(['pendingOps'], 'readwrite');
    const store = tx.objectStore('pendingOps');
    
    await store.add({
      documentId: this.documentId,
      operation,
      timestamp: Date.now()
    });
  }
  
  // Get all pending operations
  async getPendingOperations() {
    const tx = this.db.transaction(['pendingOps'], 'readonly');
    const store = tx.objectStore('pendingOps');
    const index = store.index('documentId');
    
    return new Promise((resolve, reject) => {
      const request = index.getAll(this.documentId);
      request.onsuccess = () => resolve(request.result);
      request.onerror = reject;
    });
  }
  
  // Clear synced operations
  async clearPendingOperations() {
    const tx = this.db.transaction(['pendingOps'], 'readwrite');
    const store = tx.objectStore('pendingOps');
    const index = store.index('documentId');
    
    const request = index.openCursor(this.documentId);
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        cursor.delete();
        cursor.continue();
      }
    };
  }
}

Component 5: Presence & Awareness

// Track cursor positions and user presence
class AwarenessManager {
  constructor(provider) {
    this.provider = provider;
    this.awareness = provider.awareness;
    this.localState = {
      user: null,
      cursor: null,
      selection: null,
      color: this.generateColor()
    };
  }
  
  setUser(user) {
    this.localState.user = user;
    this.updateAwareness();
  }
  
  updateCursor(position) {
    this.localState.cursor = position;
    this.updateAwareness();
  }
  
  updateSelection(range) {
    this.localState.selection = range;
    this.updateAwareness();
  }
  
  updateAwareness() {
    this.awareness.setLocalState(this.localState);
  }
  
  // Get all remote user states
  getRemoteStates() {
    const states = [];
    this.awareness.getStates().forEach((state, clientId) => {
      if (clientId !== this.awareness.clientID) {
        states.push({ ...state, clientId });
      }
    });
    return states;
  }
  
  generateColor() {
    const colors = [
      '#FF6B6B', '#4ECDC4', '#45B7D1', 
      '#FFA07A', '#98D8C8', '#F7DC6F'
    ];
    return colors[Math.floor(Math.random() * colors.length)];
  }
}

// Render remote cursors
function RemoteCursors({ awareness }) {
  const [cursors, setCursors] = useState([]);
  
  useEffect(() => {
    const updateCursors = () => {
      const states = Array.from(awareness.getStates().values())
        .filter(state => state.cursor !== null);
      setCursors(states);
    };
    
    awareness.on('change', updateCursors);
    updateCursors();
    
    return () => awareness.off('change', updateCursors);
  }, [awareness]);
  
  return (
    <>
      {cursors.map(state => (
        <Cursor
          key={state.clientId}
          position={state.cursor}
          color={state.color}
          user={state.user}
        />
      ))}
    </>
  );
}

Component 6: Performance Optimizations

// Virtual scrolling for large documents
function LargeDocumentEditor({ doc }) {
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 100 });
  const containerRef = useRef();
  
  // Only render visible lines
  const visibleLines = useMemo(() => {
    return doc.lines.slice(visibleRange.start, visibleRange.end);
  }, [doc, visibleRange]);
  
  // Update visible range on scroll
  useEffect(() => {
    const container = containerRef.current;
    
    const handleScroll = throttle(() => {
      const scrollTop = container.scrollTop;
      const lineHeight = 20; // pixels
      const viewportHeight = container.clientHeight;
      
      const start = Math.floor(scrollTop / lineHeight) - 10; // buffer
      const end = Math.ceil((scrollTop + viewportHeight) / lineHeight) + 10;
      
      setVisibleRange({
        start: Math.max(0, start),
        end: Math.min(doc.lines.length, end)
      });
    }, 100);
    
    container.addEventListener('scroll', handleScroll);
    return () => container.removeEventListener('scroll', handleScroll);
  }, [doc.lines.length]);
  
  return (
    <div ref={containerRef} style={{ height: '100vh', overflow: 'auto' }}>
      <div style={{ height: doc.lines.length * 20 }}>
        <div style={{ transform: `translateY(${visibleRange.start * 20}px)` }}>
          {visibleLines.map((line, i) => (
            <Line key={visibleRange.start + i} content={line} />
          ))}
        </div>
      </div>
    </div>
  );
}

// Debounce sync operations
function useDebouncedSync(syncFn, delay = 300) {
  const timeoutRef = useRef();
  const pendingOpsRef = useRef([]);
  
  return useCallback((operation) => {
    pendingOpsRef.current.push(operation);
    
    clearTimeout(timeoutRef.current);
    
    timeoutRef.current = setTimeout(() => {
      if (pendingOpsRef.current.length > 0) {
        syncFn(pendingOpsRef.current);
        pendingOpsRef.current = [];
      }
    }, delay);
  }, [syncFn, delay]);
}

Backend Architecture (Brief Overview):

// WebSocket server (Node.js)
const WebSocket = require('ws');
const Redis = require('redis');

class CollaborationServer {
  constructor() {
    this.wss = new WebSocket.Server({ port: 8080 });
    this.redis = Redis.createClient();
    this.rooms = new Map(); // documentId -> Set of clients
    
    this.wss.on('connection', (ws, req) => {
      this.handleConnection(ws, req);
    });
  }
  
  handleConnection(ws, req) {
    const documentId = this.extractDocumentId(req.url);
    
    // Add client to room
    if (!this.rooms.has(documentId)) {
      this.rooms.set(documentId, new Set());
    }
    this.rooms.get(documentId).add(ws);
    
    // Handle messages
    ws.on('message', async (data) => {
      const message = JSON.parse(data);
      
      switch (message.type) {
        case 'operation':
          await this.handleOperation(documentId, message, ws);
          break;
        case 'sync-request':
          await this.handleSyncRequest(documentId, message, ws);
          break;
      }
    });
    
    // Cleanup on disconnect
    ws.on('close', () => {
      this.rooms.get(documentId).delete(ws);
    });
  }
  
  async handleOperation(documentId, message, sender) {
    // Save to Redis for persistence
    await this.redis.lpush(
      `doc:${documentId}:ops`,
      JSON.stringify(message.operation)
    );
    
    // Broadcast to all clients in room except sender
    const clients = this.rooms.get(documentId);
    clients.forEach(client => {
      if (client !== sender && client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({
          type: 'update',
          operation: message.operation
        }));
      }
    });
    
    // Send acknowledgment
    sender.send(JSON.stringify({
      type: 'acknowledgment',
      operationId: message.operation.id
    }));
  }
  
  async handleSyncRequest(documentId, message, client) {
    // Get operations since client's revision
    const ops = await this.redis.lrange(
      `doc:${documentId}:ops`,
      message.revision,
      -1
    );
    
    client.send(JSON.stringify({
      type: 'sync-response',
      operations: ops.map(op => JSON.parse(op))
    }));
  }
}

Scalability Considerations:

  1. Sharding by Document: Different documents on different servers
  2. Redis Pub/Sub: For broadcasting across multiple server instances
  3. Operation Compaction: Periodically compress operation history
  4. Snapshot Storage: Save full document state every N operations
// Load balancing with sticky sessions
// nginx config
upstream collaboration_servers {
  ip_hash; # Sticky sessions based on IP
  server server1:8080;
  server server2:8080;
  server server3:8080;
}

// Or use Redis for session sharing
class DistributedCollaboration {
  constructor() {
    this.pubsub = Redis.createClient();
    this.subscriber = Redis.createClient();
    
    // Subscribe to document channels
    this.subscriber.on('message', (channel, message) => {
      const documentId = channel.replace('doc:', '');
      this.broadcastToLocalClients(documentId, message);
    });
  }
  
  async handleOperation(documentId, operation) {
    // Publish to Redis for other servers
    await this.pubsub.publish(
      `doc:${documentId}`,
      JSON.stringify(operation)
    );
    
    // Also broadcast to local clients
    this.broadcastToLocalClients(documentId, operation);
  }
}

Key Design Decisions:

  1. CRDT vs OT: CRDT chosen for:

    • No server authority needed
    • Better offline support
    • Simpler conflict resolution
    • Trade-off: Larger payload size
  2. WebSocket vs HTTP polling:

    • WebSocket for real-time, bi-directional
    • Fallback to long-polling for old browsers
  3. Operation-based vs State-based sync:

    • Operation-based for efficiency
    • Periodic state snapshots for recovery
  4. Client-side vs Server-side rendering:

    • Client-side for editor (needs interactivity)
    • Server-side for initial load and SEO

Question 11: How would you design a frontend monitoring and observability system?

Requirements:

  1. Performance metrics (Core Web Vitals)
  2. Error tracking and reporting
  3. User session replay
  4. Real-time alerts
  5. Custom business metrics
  6. Privacy compliance

Architecture Overview:

// Core monitoring SDK
class FrontendMonitor {
  constructor(config) {
    this.config = config;
    this.sessionId = this.generateSessionId();
    this.userId = null;
    this.queue = [];
    this.flushInterval = 10000; // 10 seconds
    
    this.initializeMonitoring();
  }
  
  initializeMonitoring() {
    this.trackPerformance();
    this.trackErrors();
    this.trackUserInteractions();
    this.trackNetworkRequests();
    
    // Flush queue periodically
    setInterval(() => this.flush(), this.flushInterval);
    
    // Flush on page unload
    window.addEventListener('beforeunload', () => this.flush(true));
  }
  
  // 1. Performance Monitoring
  trackPerformance() {
    // Core Web Vitals
    this.trackLCP(); // Largest Contentful Paint
    this.trackFID(); // First Input Delay
    this.trackCLS(); // Cumulative Layout Shift
    this.trackFCP(); // First Contentful Paint
    this.trackTTFB(); // Time to First Byte
  }
  
  trackLCP() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      
      this.record({
        type: 'performance',
        metric: 'LCP',
        value: lastEntry.renderTime || lastEntry.loadTime,
        timestamp: Date.now(),
        url: window.location.href,
        sessionId: this.sessionId
      });
    });
    
    observer.observe({ entryTypes: ['largest-contentful-paint'] });
  }
  
  trackFID() {
    const observer = new PerformanceObserver((list) => {
      const entry = list.getEntries()[0];
      
      this.record({
        type: 'performance',
        metric: 'FID',
        value: entry.processingStart - entry.startTime,
        timestamp: Date.now(),
        sessionId: this.sessionId
      });
    });
    
    observer.observe({ entryTypes: ['first-input'] });
  }
  
  trackCLS() {
    let clsScore = 0;
    
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsScore += entry.value;
        }
      }
      
      this.record({
        type: 'performance',
        metric: 'CLS',
        value: clsScore,
        timestamp: Date.now(),
        sessionId: this.sessionId
      });
    });
    
    observer.observe({ entryTypes: ['layout-shift'] });
  }
  
  // Navigation timing
  trackPageLoad() {
    window.addEventListener('load', () => {
      setTimeout(() => {
        const timing = performance.getEntriesByType('navigation')[0];
        
        this.record({
          type: 'performance',
          metric: 'page-load',
          data: {
            dns: timing.domainLookupEnd - timing.domainLookupStart,
            tcp: timing.connectEnd - timing.connectStart,
            ttfb: timing.responseStart - timing.requestStart,
            download: timing.responseEnd - timing.responseStart,
            domInteractive: timing.domInteractive - timing.fetchStart,
            domComplete: timing.domComplete - timing.fetchStart,
            loadComplete: timing.loadEventEnd - timing.fetchStart
          },
          timestamp: Date.now(),
          sessionId: this.sessionId
        });
      }, 0);
    });
  }
  
  // 2. Error Tracking
  trackErrors() {
    // JavaScript errors
    window.addEventListener('error', (event) => {
      this.record({
        type: 'error',
        subtype: 'javascript',
        message: event.message,
        stack: event.error?.stack,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        timestamp: Date.now(),
        sessionId: this.sessionId,
        userId: this.userId,
        url: window.location.href,
        userAgent: navigator.userAgent
      });
    });
    
    // Unhandled promise rejections
    window.addEventListener('unhandledrejection', (event) => {
      this.record({
        type: 'error',
        subtype: 'unhandled-rejection',
        message: event.reason?.message || String(event.reason),
        stack: event.reason?.stack,
        timestamp: Date.now(),
        sessionId: this.sessionId
      });
    });
    
    // React error boundaries (if using React)
    this.setupReactErrorBoundary();
  }
  
  setupReactErrorBoundary() {
    // Hook into React error boundary
    if (window.React) {
      const originalError = console.error;
      console.error = (...args) => {
        if (args[0]?.includes?.('React')) {
          this.record({
            type: 'error',
            subtype: 'react',
            message: args.join(' '),
            timestamp: Date.now(),
            sessionId: this.sessionId
          });
        }
        originalError.apply(console, args);
      };
    }
  }
  
  // 3. User Interaction Tracking
  trackUserInteractions() {
    // Click tracking
    document.addEventListener('click', (event) => {
      const target = event.target;
      
      this.record({
        type: 'interaction',
        subtype: 'click',
        element: this.getElementIdentifier(target),
        x: event.clientX,
        y: event.clientY,
        timestamp: Date.now(),
        sessionId: this.sessionId
      });
    }, true);
    
    // Form submissions
    document.addEventListener('submit', (event) => {
      const form = event.target;
      
      this.record({
        type: 'interaction',
        subtype: 'form-submit',
        formId: form.id || form.name,
        action: form.action,
        timestamp: Date.now(),
        sessionId: this.sessionId
      });
    }, true);
    
    // Page visibility
    document.addEventListener('visibilitychange', () => {
      this.record({
        type: 'interaction',
        subtype: 'visibility',
        hidden: document.hidden,
        timestamp: Date.now(),
        sessionId: this.sessionId
      });
    });
  }
  
  // 4. Network Request Tracking
  trackNetworkRequests() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
          this.record({
            type: 'network',
            url: entry.name,
            method: entry.initiatorType,
            duration: entry.duration,
            transferSize: entry.transferSize,
            encodedBodySize: entry.encodedBodySize,
            decodedBodySize: entry.decodedBodySize,
            timestamp: Date.now(),
            sessionId: this.sessionId
          });
        }
      }
    });
    
    observer.observe({ entryTypes: ['resource'] });
    
    // Intercept fetch
    const originalFetch = window.fetch;
    window.fetch = async (...args) => {
      const startTime = performance.now();
      
      try {
        const response = await originalFetch(...args);
        const duration = performance.now() - startTime;
        
        this.record({
          type: 'api-call',
          url: args[0],
          method: args[1]?.method || 'GET',
          status: response.status,
          duration,
          timestamp: Date.now(),
          sessionId: this.sessionId
        });
        
        return response;
      } catch (error) {
        const duration = performance.now() - startTime;
        
        this.record({
          type: 'api-error',
          url: args[0],
          method: args[1]?.method || 'GET',
          error: error.message,
          duration,
          timestamp: Date.now(),
          sessionId: this.sessionId
        });
        
        throw error;
      }
    };
  }
  
  // 5. Custom Metrics
  recordCustomMetric(name, value, metadata = {}) {
    this.record({
      type: 'custom-metric',
      name,
      value,
      metadata,
      timestamp: Date.now(),
      sessionId: this.sessionId,
      userId: this.userId
    });
  }
  
  // 6. Session Replay (Simplified)
  startSessionReplay() {
    const events = [];
    
    // Capture DOM mutations
    const observer = new MutationObserver((mutations) => {
      const simplified = mutations.map(m => ({
        type: m.type,
        target: this.getElementIdentifier(m.target),
        timestamp: Date.now()
      }));
      
      events.push(...simplified);
      
      // Limit size
      if (events.length > 1000) {
        this.sendReplayChunk(events.splice(0, 500));
      }
    });
    
    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeOldValue: false,
      characterData: true
    });
    
    // Capture initial state
    this.record({
      type: 'replay-start',
      html: this.sanitizeHTML(document.documentElement.outerHTML),
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight
      },
      sessionId: this.sessionId,
      timestamp: Date.now()
    });
  }
  
  // Helper methods
  getElementIdentifier(element) {
    if (element.id) return `#${element.id}`;
    if (element.className) return `.${element.className.split(' ')[0]}`;
    return element.tagName.toLowerCase();
  }
  
  sanitizeHTML(html) {
    // Remove sensitive data (passwords, tokens, etc.)
    return html
      .replace(/password="[^"]*"/g, 'password="***"')
      .replace(/token="[^"]*"/g, 'token="***"')
      .replace(/<input[^>]*type="password"[^>]*>/g, '<input type="password" value="***">');
  }
  
  generateSessionId() {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
  
  record(data) {
    // Add common fields
    const enriched = {
      ...data,
      projectId: this.config.projectId,
      environment: this.config.environment,
      version: this.config.version,
      page: {
        url: window.location.href,
        referrer: document.referrer,
        title: document.title
      },
      device: {
        userAgent: navigator.userAgent,
        viewport: {
          width: window.innerWidth,
          height: window.innerHeight
        },
        connection: navigator.connection?.effectiveType
      }
    };
    
    this.queue.push(enriched);
    
    // Immediate flush for critical events
    if (data.type === 'error' || data.type === 'api-error') {
      this.flush();
    }
  }
  
  async flush(sync = false) {
    if (this.queue.length === 0) return;
    
    const batch = this.queue.splice(0, this.queue.length);
    
    const payload = {
      batch,
      timestamp: Date.now(),
      sessionId: this.sessionId
    };
    
    if (sync) {
      // Use sendBeacon for reliable delivery on page unload
      navigator.sendBeacon(
        this.config.endpoint,
        JSON.stringify(payload)
      );
    } else {
      try {
        await fetch(this.config.endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
          keepalive: true
        });
      } catch (error) {
        // Failed to send - could retry or log locally
        console.error('Failed to send monitoring data:', error);
      }
    }
  }
  
  // Public API
  setUserId(userId) {
    this.userId = userId;
  }
  
  addContext(key, value) {
    this.record({
      type: 'context',
      key,
      value,
      timestamp: Date.now()
    });
  }
}

// Initialize monitoring
const monitor = new FrontendMonitor({
  projectId: 'my-app',
  environment: 'production',
  version: '1.2.3',
  endpoint: 'https://monitoring.example.com/ingest'
});

React Integration:

// Error Boundary with monitoring
class MonitoredErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    // Send to monitoring system
    monitor.record({
      type: 'error',
      subtype: 'react-boundary',
      message: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
      timestamp: Date.now()
    });
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Something went wrong</h1>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Custom hook for tracking
function useMonitoring() {
  return {
    trackEvent: (name, properties) => {
      monitor.recordCustomMetric(name, properties);
    },
    
    trackTiming: (name, duration) => {
      monitor.recordCustomMetric(`timing.${name}`, duration);
    },
    
    setUser: (userId) => {
      monitor.setUserId(userId);
    }
  };
}

// Track component render performance
function useRenderTracking(componentName) {
  useEffect(() => {
    const startTime = performance.now();
    
    return () => {
      const duration = performance.now() - startTime;
      monitor.recordCustomMetric('component-render', duration, {
        component: componentName
      });
    };
  });
}

Backend Processing Pipeline:

// Ingest service (Node.js)
class MonitoringIngestService {
  constructor() {
    this.kafka = new Kafka({
      brokers: ['kafka:9092']
    });
    this.producer = this.kafka.producer();
  }
  
  async handleIngest(req, res) {
    const { batch } = req.body;
    
    // Validate and enrich
    const enriched = batch.map(event => ({
      ...event,
      receivedAt: Date.now(),
      ip: req.ip,
      geo: this.lookupGeo(req.ip)
    }));
    
    // Send to Kafka for processing
    await this.producer.send({
      topic: 'frontend-events',
      messages: enriched.map(event => ({
        key: event.sessionId,
        value: JSON.stringify(event)
      }))
    });
    
    res.status(200).json({ success: true });
  }
  
  lookupGeo(ip) {
    // GeoIP lookup
    return { country: 'US', city: 'New York' };
  }
}

// Processing worker
class EventProcessor {
  constructor() {
    this.kafka = new Kafka({ brokers: ['kafka:9092'] });
    this.consumer = this.kafka.consumer({ groupId: 'event-processor' });
    this.timescale = new TimescaleDB();
    this.redis = new Redis();
  }
  
  async start() {
    await this.consumer.subscribe({ topic: 'frontend-events' });
    
    await this.consumer.run({
      eachMessage: async ({ message }) => {
        const event = JSON.parse(message.value);
        
        // Route by type
        switch (event.type) {
          case 'error':
            await this.processError(event);
            break;
          case 'performance':
            await this.processPerformance(event);
            break;
          case 'interaction':
            await this.processInteraction(event);
            break;
        }
      }
    });
  }
  
  async processError(event) {
    // Store in TimescaleDB
    await this.timescale.query(`
      INSERT INTO errors (
        session_id, user_id, message, stack, url, timestamp
      ) VALUES ($1, $2, $3, $4, $5, $6)
    `, [
      event.sessionId,
      event.userId,
      event.message,
      event.stack,
      event.url,
      event.timestamp
    ]);
    
    // Check for alerts
    const errorCount = await this.redis.incr(
      `errors:${event.message}:${Date.now() / 60000}`
    );
    
    if (errorCount > 10) {
      await this.sendAlert({
        type: 'error-spike',
        message: `Error spike detected: ${event.message}`,
        count: errorCount
      });
    }
    
    // Group similar errors
    const errorHash = this.hashError(event);
    await this.redis.hincrby('error-groups', errorHash, 1);
  }
  
  async processPerformance(event) {
    // Store metric
    await this.timescale.query(`
      INSERT INTO performance_metrics (
        session_id, metric, value, url, timestamp
      ) VALUES ($1, $2, $3, $4, $5)
    `, [
      event.sessionId,
      event.metric,
      event.value,
      event.url,
      event.timestamp
    ]);
    
    // Update real-time aggregates
    await this.redis.zadd(
      `perf:${event.metric}:recent`,
      event.timestamp,
      JSON.stringify({ value: event.value, url: event.url })
    );
    
    // Check thresholds
    if (event.metric === 'LCP' && event.value > 2500) {
      await this.sendAlert({
        type: 'performance-degradation',
        metric: 'LCP',
        value: event.value,
        threshold: 2500,
        url: event.url
      });
    }
  }
  
  hashError(event) {
    const crypto = require('crypto');
    return crypto
      .createHash('md5')
      .update(event.message + event.stack?.split('\n')[0])
      .digest('hex');
  }
  
  async sendAlert(alert) {
    // Send to alerting system (PagerDuty, Slack, etc.)
    await fetch('https://hooks.slack.com/services/XXX', {
      method: 'POST',
      body: JSON.stringify({
        text: `⚠️ ${alert.message}`
      })
    });
  }
}

Dashboard & Query API:

// Query service
class MonitoringQueryService {
  constructor() {
    this.timescale = new TimescaleDB();
  }
  
  // Get performance metrics over time
  async getPerformanceMetrics(metric, startTime, endTime) {
    const results = await this.timescale.query(`
      SELECT 
        time_bucket('5 minutes', timestamp) AS bucket,
        percentile_cont(0.5) WITHIN GROUP (ORDER BY value) AS p50,
        percentile_cont(0.75) WITHIN GROUP (ORDER BY value) AS p75,
        percentile_cont(0.95) WITHIN GROUP (ORDER BY value) AS p95,
        percentile_cont(0.99) WITHIN GROUP (ORDER BY value) AS p99
      FROM performance_metrics
      WHERE metric = $1
        AND timestamp >= $2
        AND timestamp <= $3
      GROUP BY bucket
      ORDER BY bucket
    `, [metric, startTime, endTime]);
    
    return results.rows;
  }
  
  // Get error rate
  async getErrorRate(startTime, endTime) {
    const results = await this.timescale.query(`
      SELECT 
        time_bucket('5 minutes', timestamp) AS bucket,
        COUNT(*) as error_count
      FROM errors
      WHERE timestamp >= $1 AND timestamp <= $2
      GROUP BY bucket
      ORDER BY bucket
    `, [startTime, endTime]);
    
    return results.rows;
  }
  
  // Get top errors
  async getTopErrors(limit = 10) {
    const results = await this.timescale.query(`
      SELECT 
        message,
        COUNT(*) as count,
        COUNT(DISTINCT session_id) as affected_sessions,
        MAX(timestamp) as last_seen
      FROM errors
      WHERE timestamp > NOW() - INTERVAL '24 hours'
      GROUP BY message
      ORDER BY count DESC
      LIMIT $1
    `, [limit]);
    
    return results.rows;
  }
  
  // User journey analysis
  async getUserJourney(sessionId) {
    const results = await this.timescale.query(`
      SELECT type, subtype, url, timestamp, data
      FROM events
      WHERE session_id = $1
      ORDER BY timestamp
    `, [sessionId]);
    
    return results.rows;
  }
}

Privacy & Compliance:

// Data sanitization for GDPR/CCPA
class PrivacyManager {
  sanitizeEvent(event) {
    // Remove PII
    const sanitized = { ...event };
    
    // Mask email addresses
    if (sanitized.message) {
      sanitized.message = sanitized.message.replace(
        /[\w.-]+@[\w.-]+\.\w+/g,
        '***@***.***'
      );
    }
    
    // Remove query parameters with sensitive data
    if (sanitized.url) {
      const url = new URL(sanitized.url);
      const sensitiveParams = ['token', 'key', 'password', 'email'];
      
      sensitiveParams.forEach(param => {
        if (url.searchParams.has(param)) {
          url.searchParams.set(param, '***');
        }
      });
      
      sanitized.url = url.toString();
    }
    
    // Hash IP addresses
    if (sanitized.ip) {
      sanitized.ip = this.hashIP(sanitized.ip);
    }
    
    return sanitized;
  }
  
  hashIP(ip) {
    const crypto = require('crypto');
    return crypto.createHash('sha256').update(ip).digest('hex').substr(0, 16);
  }
  
  // GDPR: Delete user data
  async deleteUserData(userId) {
    await this.timescale.query(`
      DELETE FROM events WHERE user_id = $1
    `, [userId]);
    
    await this.timescale.query(`
      DELETE FROM errors WHERE user_id = $1
    `, [userId]);
  }
  
  // GDPR: Export user data
  async exportUserData(userId) {
    const events = await this.timescale.query(`
      SELECT * FROM events WHERE user_id = $1
    `, [userId]);
    
    return events.rows;
  }
}

Key Design Decisions:

  1. Client-side sampling: Only send 10% of performance metrics to reduce load
  2. Batching: Group events to reduce network requests
  3. Priority queuing: Errors flush immediately, metrics batch
  4. Privacy-first: Sanitize before sending, hash IPs, mask PII
  5. Time-series DB: TimescaleDB for efficient metric storage
  6. Real-time processing: Kafka for stream processing
  7. Retention policies: 90 days raw data, 1 year aggregates

Question 12: Design a micro-frontend architecture for a large e-commerce platform

Requirements:

  1. Multiple teams working independently
  2. Different tech stacks per team
  3. Shared design system
  4. Performance (no massive bundle)
  5. Gradual migration from monolith
  6. SEO-friendly

Architecture Patterns:

Pattern 1: Build-Time Integration (Not Recommended)

// ❌ Compile-time integration - loses team autonomy
import ProductList from '@team-catalog/product-list';
import Checkout from '@team-checkout/checkout';

function App() {
  return (
    <>
      <ProductList />
      <Checkout />
    </>
  );
}

Problems:

  • All teams must use same framework
  • Coordinated deployments
  • Version conflicts

Pattern 2: Run-Time Integration via Module Federation (Recommended)

// Webpack Module Federation
// Host application (shell)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        productCatalog: 'productCatalog@http://localhost:3001/remoteEntry.js',
        checkout: 'checkout@http://localhost:3002/remoteEntry.js',
        userProfile: 'userProfile@http://localhost:3003/remoteEntry.js'
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
      }
    })
  ]
};

// Shell application
import React, { lazy, Suspense } from 'react';

const ProductCatalog = lazy(() => import('productCatalog/ProductList'));
const Checkout = lazy(() => import('checkout/CheckoutFlow'));
const UserProfile = lazy(() => import('userProfile/ProfilePage'));

function App() {
  return (
    <BrowserRouter>
      <Layout>
        <Suspense fallback={<Spinner />}>
          <Routes>
            <Route path="/products" element={<ProductCatalog />} />
            <Route path="/checkout" element={<Checkout />} />
            <Route path="/profile" element={<UserProfile />} />
          </Routes>
        </Suspense>
      </Layout>
    </BrowserRouter>
  );
}

// Remote application (Product Catalog)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'productCatalog',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ]
};

Benefits:

  • Independent deployments
  • Load on demand
  • Share dependencies
  • Can use same framework

Drawbacks:

  • Still tied to webpack
  • Shared dependencies must match versions
  • Complex configuration

Pattern 3: Web Components (Framework Agnostic)

// Product Catalog (React)
import React from 'react';
import ReactDOM from 'react-dom/client';

class ProductCatalogElement extends HTMLElement {
  connectedCallback() {
    const mountPoint = document.createElement('div');
    this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
    
    const root = ReactDOM.createRoot(mountPoint);
    root.render(<ProductCatalog 
      apiUrl={this.getAttribute('api-url')}
      onProductClick={this.handleProductClick}
    />);
  }
  
  handleProductClick = (productId) => {
    this.dispatchEvent(new CustomEvent('product-selected', {
      detail: { productId },
      bubbles: true
    }));
  };
}

customElements.define('product-catalog', ProductCatalogElement);

// Checkout (Vue)
import { createApp } from 'vue';
import CheckoutApp from './Checkout.vue';

class CheckoutElement extends HTMLElement {
  connectedCallback() {
    const mountPoint = document.createElement('div');
    this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
    
    createApp(CheckoutApp, {
      cartId: this.getAttribute('cart-id')
    }).mount(mountPoint);
  }
}

customElements.define('checkout-flow', CheckoutElement);

// Host application (any framework or vanilla)
<html>
  <body>
    <div id="app">
      <navigation-bar></navigation-bar>
      
      <product-catalog 
        api-url="https://api.example.com/products">
      </product-catalog>
      
      <checkout-flow 
        cart-id="abc123">
      </checkout-flow>
    </div>
    
    <script src="https://cdn.example.com/product-catalog/1.2.3/bundle.js"></script>
    <script src="https://cdn.example.com/checkout/2.0.1/bundle.js"></script>
  </body>
</html>

Benefits:

  • True framework independence
  • Shadow DOM encapsulation
  • Standard web platform

Drawbacks:

  • No shared React context
  • Styling isolation (good and bad)
  • Each micro-frontend loads full framework

Pattern 4: iFrame (Simple but Limited)

// Host
function App() {
  return (
    <div>
      <iframe 
        src="https://products.example.com"
        style={{ border: 'none', width: '100%', height: '600px' }}
        sandbox="allow-scripts allow-same-origin"
      />
    </div>
  );
}

// Communication via postMessage
window.parent.postMessage({
  type: 'product-selected',
  productId: '123'
}, '*');

Benefits:

  • Complete isolation
  • Simple to implement
  • Zero version conflicts

Drawbacks:

  • Performance overhead
  • SEO challenges
  • Complex communication
  • Awkward routing

Recommended Hybrid Architecture:

┌─────────────────────────────────────────────┐
│          Shell Application (Host)           │
│  - Routing                                  │
│  - Authentication                           │
│  - Shared UI components                     │
│  - Global state management                  │
└─────────────────┬───────────────────────────┘
                  │
        ┌─────────┼─────────┐
        │         │         │
┌───────▼──┐ ┌───▼─────┐ ┌─▼────────┐
│ Product  │ │Checkout │ │  User    │
│ Catalog  │ │  Flow   │ │ Profile  │
│ (React)  │ │  (Vue)  │ │ (Svelte) │
└──────────┘ └─────────┘ └──────────┘

Implementation:

// 1. Shell Application
import { createBrowserRouter } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        path: 'products/*',
        lazy: () => loadMicroFrontend('productCatalog', '/remoteEntry.js')
      },
      {
        path: 'checkout/*',
        lazy: () => loadMicroFrontend('checkout', '/remoteEntry.js')
      },
      {
        path: 'profile/*',
        lazy: () => loadMicroFrontend('userProfile', '/remoteEntry.js')
      }
    ]
  }
]);

// Dynamic micro-frontend loader
async function loadMicroFrontend(name, entry) {
  // Load remote entry
  await loadScript(`https://${name}.example.com${entry}`);
  
  // Get the container
  const container = window[name];
  await container.init(__webpack_share_scopes__.default);
  
  // Get the module
  const factory = await container.get('./App');
  const Module = factory();
  
  return {
    Component: Module.default
  };
}

// 2. Shared Event Bus
class MicroFrontendEventBus {
  constructor() {
    this.listeners = new Map();
  }
  
  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event).push(callback);
  }
  
  off(event, callback) {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    }
  }
  
  emit(event, data) {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      callbacks.forEach(callback => callback(data));
    }
  }
}

// Global event bus
window.microFrontendBus = new MicroFrontendEventBus();

// Usage in micro-frontends
// Product Catalog
window.microFrontendBus.emit('product:added-to-cart', {
  productId: '123',
  quantity: 1
});

// Checkout
window.microFrontendBus.on('product:added-to-cart', (data) => {
  updateCart(data);
});

// 3. Shared State Management
class SharedStateManager {
  constructor() {
    this.state = {};
    this.subscribers = new Map();
  }
  
  setState(key, value) {
    this.state[key] = value;
    this.notify(key, value);
  }
  
  getState(key) {
    return this.state[key];
  }
  
  subscribe(key, callback) {
    if (!this.subscribers.has(key)) {
      this.subscribers.set(key, []);
    }
    this.subscribers.get(key).push(callback);
    
    // Return unsubscribe function
    return () => {
      const callbacks = this.subscribers.get(key);
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    };
  }
  
  notify(key, value) {
    const callbacks = this.subscribers.get(key);
    if (callbacks) {
      callbacks.forEach(callback => callback(value));
    }
  }
}

window.sharedState = new SharedStateManager();

// Usage
// Set user in shell
window.sharedState.setState('user', { id: '123', name: 'John' });

// Subscribe in micro-frontend
const unsubscribe = window.sharedState.subscribe('user', (user) => {
  console.log('User updated:', user);
});

// 4. Shared Design System
// Published as npm package and imported by all micro-frontends
import { Button, Input, Card } from '@company/design-system';

// Or loaded as web components
<ds-button variant="primary">Click me</ds-button>

// 5. API Gateway Pattern
class APIClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.token = null;
  }
  
  setToken(token) {
    this.token = token;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const headers = {
      'Content-Type': 'application/json',
      ...(this.token && { 'Authorization': `Bearer ${this.token}` }),
      ...options.headers
    };
    
    const response = await fetch(url, {
      ...options,
      headers
    });
    
    if (!response.ok) {
      throw new Error(`API Error: ${response.statusText}`);
    }
    
    return response.json();
  }
}

// Shared API client
window.apiClient = new APIClient('https://api.example.com');

// Usage in micro-frontends
const products = await window.apiClient.request('/products');

Performance Optimizations:

// 1. Lazy loading with preloading
function preloadMicroFrontend(name) {
  const link = document.createElement('link');
  link.rel = 'prefetch';
  link.href = `https://${name}.example.com/remoteEntry.js`;
  document.head.appendChild(link);
}

// Preload on hover
<Link 
  to="/products"
  onMouseEnter={() => preloadMicroFrontend('productCatalog')}
>
  Products
</Link>

// 2. Service Worker for caching
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('remoteEntry.js')) {
    event.respondWith(
      caches.match(event.request).then(response => {
        return response || fetch(event.request).then(fetchResponse => {
          return caches.open('micro-frontends-v1').then(cache => {
            cache.put(event.request, fetchResponse.clone());
            return fetchResponse;
          });
        });
      })
    );
  }
});

// 3. Code splitting within micro-frontends
// Product Catalog micro-frontend
const ProductDetail = lazy(() => import('./ProductDetail'));
const ProductList = lazy(() => import('./ProductList'));

// 4. Shared chunk optimization
// webpack config
optimization: {
  splitChunks: {
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
        name: 'vendor',
        chunks: 'all'
      }
    }
  }
}

Deployment Strategy:

// Version management
const MICRO_FRONTEND_VERSIONS = {
  productCatalog: {
    stable: '1.5.2',
    canary: '1.6.0-beta.1'
  },
  checkout: {
    stable: '2.1.0',
    canary: '2.2.0-rc.1'
  }
};

// Feature flags for gradual rollout
function getMicroFrontendVersion(name, userId) {
  const versions = MICRO_FRONTEND_VERSIONS[name];
  
  // 10% canary deployment
  if (isInCanaryGroup(userId, 0.1)) {
    return versions.canary;
  }
  
  return versions.stable;
}

function loadMicroFrontend(name, userId) {
  const version = getMicroFrontendVersion(name, userId);
  return loadScript(`https://${name}.example.com/${version}/remoteEntry.js`);
}

Testing Strategy:

// 1. Contract testing between shell and micro-frontends
// micro-frontend-contracts.test.js
describe('Product Catalog Contract', () => {
  it('should expose ProductList component', async () => {
    const module = await import('productCatalog/ProductList');
    expect(module.default).toBeDefined();
    expect(typeof module.default).toBe('function');
  });
  
  it('should emit product-selected event', () => {
    const callback = jest.fn();
    window.microFrontendBus.on('product:selected', callback);
    
    // Trigger event
    window.microFrontendBus.emit('product:selected', { id: '123' });
    
    expect(callback).toHaveBeenCalledWith({ id: '123' });
  });
});

// 2. Integration testing
describe('Shell Integration', () => {
  it('should load and render micro-frontend', async () => {
    render(<Shell />);
    
    await waitFor(() => {
      expect(screen.getByTestId('product-catalog')).toBeInTheDocument();
    });
  });
});

// 3. E2E testing across boundaries
// cypress/integration/checkout-flow.spec.js
describe('Checkout Flow', () => {
  it('should complete purchase from product to payment', () => {
    cy.visit('/products');
    cy.get('[data-testid="product-item"]').first().click();
    cy.get('[data-testid="add-to-cart"]').click();
    cy.get('[data-testid="checkout-button"]').click();
    
    // Now in checkout micro-frontend
    cy.get('[data-testid="payment-form"]').should('be.visible');
    cy.get('[data-testid="complete-purchase"]').click();
    
    cy.url().should('include', '/confirmation');
  });
});

Monitoring & Observability:

// Track micro-frontend loading performance
window.microFrontendBus.on('mfe:loaded', (data) => {
  window.monitor.recordCustomMetric('mfe-load-time', data.duration, {
    name: data.name,
    version: data.version
  });
});

// Track errors per micro-frontend
window.addEventListener('error', (event) => {
  const mfeName = identifyMicroFrontend(event.filename);
  
  window.monitor.record({
    type: 'error',
    microFrontend: mfeName,
    message: event.message,
    stack: event.error?.stack
  });
});

function identifyMicroFrontend(filename) {
  if (filename.includes('productCatalog')) return 'productCatalog';
  if (filename.includes('checkout')) return 'checkout';
  if (filename.includes('userProfile')) return 'userProfile';
  return 'shell';
}

Key Design Decisions Summary:

  1. Module Federation for same framework: Best balance of performance and autonomy
  2. Web Components for different frameworks: When teams need total independence
  3. Event bus for communication: Loose coupling between micro-frontends
  4. Shared state manager: For truly global state (auth, user)
  5. Design system as shared dependency: Ensures UI consistency
  6. Version management: Canary deployments, gradual rollouts
  7. API Gateway: Centralized authentication and routing
  8. Contract testing: Prevent breaking changes

Part 5: Advanced Questions

Question 13: Explain the differences between memoization strategies: React.memo, useMemo, useCallback, and manual memoization

React.memo (Component Memoization)

// Without memo - re-renders on every parent render
function ExpensiveChild({ data, onAction }) {
  console.log('Rendering ExpensiveChild');
  return <div>{expensiveCalculation(data)}</div>;
}

// With memo - only re-renders when props change
const MemoizedChild = React.memo(ExpensiveChild);

// Custom comparison function
const MemoizedChildCustom = React.memo(
  ExpensiveChild,
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return prevProps.data.id === nextProps.data.id;
  }
);

When React.memo helps:

  • Component is expensive to render
  • Component receives same props frequently
  • Component is a pure function of its props

When React.memo doesn't help:

  • Props change every render (new objects/functions)
  • Component is cheap to render
  • Component has children that change

Common Pitfall:

function Parent() {
  const [count, setCount] = useState(0);
  
  // ❌ New object every render - memo useless
  const data = { value: 'hello' };
  
  // ❌ New function every render - memo useless
  const handleClick = () => console.log('clicked');
  
  return <MemoizedChild data={data} onClick={handleClick} />;
}

// ✅ Fix with useMemo and useCallback
function Parent() {
  const [count, setCount] = useState(0);
  
  const data = useMemo(() => ({ value: 'hello' }), []);
  const handleClick = useCallback(() => console.log('clicked'), []);
  
  return <MemoizedChild data={data} onClick={handleClick} />;
}

useMemo (Value Memoization)

function ProductList({ products, filter }) {
  // ❌ Expensive calculation runs every render
  const filtered = products
    .filter(p => p.category === filter)
    .sort((a, b) => b.rating - a.rating);
  
  // ✅ Only recalculates when dependencies change
  const filtered = useMemo(() => {
    return products
      .filter(p => p.category === filter)
      .sort((a, b) => b.rating - a.rating);
  }, [products, filter]);
  
  return <div>{filtered.map(p => <Product key={p.id} {...p} />)}</div>;
}

When useMemo helps:

  • Expensive calculations (filtering, sorting large arrays)
  • Creating objects/arrays passed to memoized children
  • Avoiding unnecessary re-renders due to referential inequality

When useMemo doesn't help:

  • Simple calculations (addition, string concatenation)
  • Values that change every render anyway
  • Over-optimization without measurement

Cost-Benefit Analysis:

// ❌ Premature optimization
const sum = useMemo(() => a + b, [a, b]);
// Cost of useMemo > cost of calculation

// ✅ Worth it
const sortedAndFiltered = useMemo(() => {
  return data
    .filter(item => item.active)
    .sort((a, b) => a.value - b.value)
    .map(item => ({ ...item, computed: heavyFunction(item) }));
}, [data]);

useCallback (Function Memoization)

function ProductGrid({ products }) {
  const [selectedId, setSelectedId] = useState(null);
  
  // ❌ New function every render
  const handleSelect = (id) => {
    setSelectedId(id);
    analytics.track('product-selected', { id });
  };
  
  // ✅ Same function reference across renders
  const handleSelect = useCallback((id) => {
    setSelectedId(id);
    analytics.track('product-selected', { id });
  }, []); // Empty deps - function never changes
  
  // ✅ Updates when selectedId changes
  const handleToggle = useCallback((id) => {
    setSelectedId(prev => prev === id ? null : id);
  }, []); // Can use functional update, so no dep needed
  
  return products.map(p => (
    <MemoizedProduct 
      key={p.id} 
      product={p}
      onSelect={handleSelect}
    />
  ));
}

When useCallback helps:

  • Passing callbacks to memoized children
  • Callbacks used as dependencies in useEffect
  • Preventing child re-renders

When useCallback doesn't help:

  • Callbacks not passed to memoized components
  • Callbacks change frequently anyway
  • Simple event handlers

Common Pattern:

function SearchBar() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  // ✅ Callback is dependency in useEffect
  const fetchResults = useCallback(async (searchQuery) => {
    const data = await api.search(searchQuery);
    setResults(data);
  }, []);
  
  useEffect(() => {
    if (query) {
      fetchResults(query);
    }
  }, [query, fetchResults]); // fetchResults won't cause re-fetch
  
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

Manual Memoization (useRef + Custom Logic)

function ManualMemoization() {
  const cache = useRef(new Map());
  
  function expensiveCalculation(input) {
    // Check cache
    if (cache.current.has(input)) {
      console.log('Cache hit');
      return cache.current.get(input);
    }
    
    // Compute
    console.log('Computing...');
    const result = /* expensive computation */ input * 2;
    
    // Store in cache
    cache.current.set(input, result);
    
    return result;
  }
  
  return <div>{expensiveCalculation(props.value)}</div>;
}

When manual memoization helps:

  • Complex caching logic
  • LRU cache needed
  • Caching across multiple renders with custom eviction
  • Fine-grained control over cache invalidation

Advanced: LRU Cache Implementation

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }
  
  get(key) {
    if (!this.cache.has(key)) return undefined;
    
    // Move to end (most recently used)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    
    return value;
  }
  
  set(key, value) {
    // Remove if exists
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    
    // Add to end
    this.cache.set(key, value);
    
    // Evict oldest if over capacity
    if (this.cache.size > this.capacity) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
  }
}

function ComponentWithLRU() {
  const cache = useRef(new LRUCache(100));
  
  function compute(input) {
    let result = cache.current.get(input);
    
    if (result === undefined) {
      result = expensiveComputation(input);
      cache.current.set(input, result);
    }
    
    return result;
  }
  
  return <div>{compute(props.data)}</div>;
}

Comparison Table:

Technique Memoizes Use Case Performance Cost
React.memo Component Prevent re-renders Props comparison
useMemo Value Expensive calculations Dependency check
useCallback Function Stable callback references Dependency check
Manual (useRef) Custom Complex caching logic Your implementation

Decision Tree:

Do you need to prevent a component re-render?
├─ Yes → Use React.memo
│   └─ Are props objects/functions?
│       └─ Yes → Also use useMemo/useCallback for props
└─ No → Is it an expensive calculation?
    ├─ Yes → Use useMemo
    └─ No → Is it a function passed to memoized child?
        ├─ Yes → Use useCallback
        └─ No → Don't memoize

Question 14: How do you handle race conditions in React applications?

Problem: Multiple Async Operations

// ❌ Race condition: Last request wins, not latest request
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => setResults(data));
      // If query changes quickly, old responses can overwrite new ones
  }, [query]);
  
  return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}

Solution 1: Cleanup Flag

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    let cancelled = false;
    
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => {
        if (!cancelled) {
          setResults(data);
        }
      });
    
    return () => {
      cancelled = true;
    };
  }, [query]);
  
  return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}

Solution 2: AbortController

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    const controller = new AbortController();
    
    fetch(`/api/search?q=${query}`, {
      signal: controller.signal
    })
      .then(r => r.json())
      .then(data => setResults(data))
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error('Search failed:', err);
        }
      });
    
    return () => {
      controller.abort();
    };
  }, [query]);
  
  return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}

Solution 3: Request ID/Version

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const requestIdRef = useRef(0);
  
  useEffect(() => {
    const requestId = ++requestIdRef.current;
    
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => {
        // Only update if this is still the latest request
        if (requestId === requestIdRef.current) {
          setResults(data);
        }
      });
  }, [query]);
  
  return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}

Solution 4: Custom Hook with Race Condition Prevention

function useAsyncData(fetchFn, deps) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let cancelled = false;
    const controller = new AbortController();
    
    async function fetchData() {
      setLoading(true);
      setError(null);
      
      try {
        const result = await fetchFn(controller.signal);
        
        if (!cancelled) {
          setData(result);
          setLoading(false);
        }
      } catch (err) {
        if (!cancelled && err.name !== 'AbortError') {
          setError(err);
          setLoading(false);
        }
      }
    }
    
    fetchData();
    
    return () => {
      cancelled = true;
      controller.abort();
    };
  }, deps);
  
  return { data, loading, error };
}

// Usage
function SearchResults({ query }) {
  const { data, loading, error } = useAsyncData(
    async (signal) => {
      const response = await fetch(`/api/search?q=${query}`, { signal });
      return response.json();
    },
    [query]
  );
  
  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  
  return <div>{data.map(r => <Result key={r.id} {...r} />)}</div>;
}

Race Condition in Optimistic Updates:

function TodoItem({ todo }) {
  const [isCompleted, setIsCompleted] = useState(todo.completed);
  
  // ❌ Race condition with rapid clicks
  async function toggleComplete() {
    setIsCompleted(!isCompleted); // Optimistic update
    
    try {
      await api.updateTodo(todo.id, { completed: !isCompleted });
    } catch (error) {
      setIsCompleted(isCompleted); // Revert
    }
  }
  
  // ✅ Fixed with request tracking
  const pendingRequestRef = useRef(null);
  
  async function toggleComplete() {
    // Cancel previous request
    if (pendingRequestRef.current) {
      pendingRequestRef.current.abort();
    }
    
    const controller = new AbortController();
    pendingRequestRef.current = controller;
    
    const newState = !isCompleted;
    setIsCompleted(newState);
    
    try {
      await api.updateTodo(
        todo.id, 
        { completed: newState },
        { signal: controller.signal }
      );
      pendingRequestRef.current = null;
    } catch (error) {
      if (error.name !== 'AbortError') {
        setIsCompleted(!newState); // Revert
      }
    }
  }
  
  return (
    <div>
      <input 
        type="checkbox" 
        checked={isCompleted}
        onChange={toggleComplete}
      />
      {todo.title}
    </div>
  );
}

Race Conditions in Parallel Requests:

// Problem: Need data from multiple sources
function UserDashboard({ userId }) {
  const [profile, setProfile] = useState(null);
  const [orders, setOrders] = useState([]);
  const [reviews, setReviews] = useState([]);
  
  useEffect(() => {
    // ❌ Three separate effects = three race conditions
    fetch(`/api/users/${userId}`).then(r => r.json()).then(setProfile);
    fetch(`/api/orders/${userId}`).then(r => r.json()).then(setOrders);
    fetch(`/api/reviews/${userId}`).then(r => r.json()).then(setReviews);
  }, [userId]);
  
  // ✅ Solution: Single effect with Promise.all
  useEffect(() => {
    let cancelled = false;
    
    Promise.all([
      fetch(`/api/users/${userId}`).then(r => r.json()),
      fetch(`/api/orders/${userId}`).then(r => r.json()),
      fetch(`/api/reviews/${userId}`).then(r => r.json())
    ]).then(([profileData, ordersData, reviewsData]) => {
      if (!cancelled) {
        setProfile(profileData);
        setOrders(ordersData);
        setReviews(reviewsData);
      }
    });
    
    return () => {
      cancelled = true;
    };
  }, [userId]);
}

Advanced: Debouncing to Prevent Race Conditions

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);
  
  return debouncedValue;
}

function SearchResults({ query }) {
  const debouncedQuery = useDebounce(query, 300);
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    if (!debouncedQuery) return;
    
    const controller = new AbortController();
    
    fetch(`/api/search?q=${debouncedQuery}`, {
      signal: controller.signal
    })
      .then(r => r.json())
      .then(setResults)
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err);
        }
      });
    
    return () => controller.abort();
  }, [debouncedQuery]);
  
  return <div>{results.map(r => <Result key={r.id} {...r} />)}</div>;
}

Conclusion

These interview questions cover the cutting edge of frontend development in 2025:

  1. React Hooks - Deep understanding of closures, memoization, and rules
  2. Server Components - New paradigm shifting SSR and data fetching
  3. Performance - Real tradeoffs in code splitting, scrolling, and Context
  4. System Design - Architecting complex systems like collaborative editors
  5. Micro-frontends - Building scalable applications with multiple teams
  6. Observability - Production monitoring and debugging
  7. Race Conditions - Handling async complexity correctly

Key Takeaways:

  • Understand not just the "what" but the "why" and "when"
  • Performance optimization requires measurement, not guessing
  • Every architectural decision has tradeoffs
  • Real-world complexity requires systematic approaches
  • Testing and monitoring are essential, not optional

Further Study:

  • React documentation on Server Components
  • Web performance APIs (PerformanceObserver, etc.)
  • Distributed systems concepts (CRDT, OT)
  • Browser internals and rendering pipeline
  • Modern JavaScript features (Temporal, Pattern Matching)

Remember: The best engineers don't just know the answers—they understand the tradeoffs and can explain their reasoning clearly.

Web DevelopmentJavaScriptreactNextJSjavascriptweb developmentNextJSReact

Related Quizzes

No related quizzes available.

Comments (0)

No comments yet. Be the first to comment!