PerformanceReact InternalsArchitectureWebpackSystem Design

Frontend II — Advanced / Real-World / System-Level

Beyond the basics. Performance optimization, React internals, architecture decisions, build systems, and the questions that separate senior engineers from mid-level devs.

90 min read14 sections
01

Frontend Performance (Deep)

🔥 This is the #1 advanced topic

Performance questions are the litmus test for senior frontend engineers. Anyone can say "use lazy loading." Interviewers want to hear you reason about metrics, trade-offs, and measurement. Always tie your answer to a real metric (LCP, FID, CLS, TTFB).

🔥 How to Optimize Frontend Performance — The Real Answer

Don't list random techniques. Structure your answer around the rendering pipeline: Network → Parse → Render → Interact. Each stage has specific bottlenecks and solutions.

Stage 1: Network — Reduce What You Send

  • Code splitting — load only what the current route needs (React.lazy, dynamic import())
  • Tree shaking — eliminate dead code at build time (requires ES modules)
  • Compression — Brotli > Gzip. Configure at CDN/server level
  • Image optimization — WebP/AVIF, responsive srcset, lazy loading with loading="lazy"
  • Font optimization — font-display: swap, subset fonts, preload critical fonts
  • CDN — serve static assets from edge locations close to users

Stage 2: Parse & Render — Don't Block the Main Thread

  • Minimize render-blocking CSS — inline critical CSS, defer non-critical
  • Defer/async scripts — defer for scripts that need DOM, async for independent scripts
  • Avoid layout thrashing — batch DOM reads before writes
  • Use content-visibility: auto for off-screen content

Stage 3: Runtime — Keep Interactions Smooth

  • Virtualize long lists (render only visible items)
  • Debounce/throttle expensive event handlers
  • Use Web Workers for CPU-heavy computation off the main thread
  • Memoize expensive React renders (React.memo, useMemo)
  • Avoid unnecessary re-renders — stable references, proper key usage

Bundle Size Reduction — Practical Playbook

Bundle size directly impacts load time. Every 100KB of JS adds ~300ms on a mid-range mobile device (parse + compile + execute). Here's the real playbook:

bundle-analysis.shbash
# Step 1: Analyze what's in your bundle
npx webpack-bundle-analyzer stats.json
# or for Next.js:
ANALYZE=true next build

# Step 2: Find the culprits (usually)
# - moment.jsreplace with day.js (70KB → 2KB)
# - lodashimport individual functions: import debounce from 'lodash/debounce'
# - iconsimport specific icons, not the entire library
# - polyfillstarget modern browsers, drop IE11 support

# Step 3: Code split aggressively
# - Route-based splitting (automatic in Next.js)
# - Component-based: React.lazy for heavy components
# - Library-based: dynamic import for rarely-used features

Critical Rendering Path — What Actually Happens

Browser receives HTML → parses to DOM → encounters CSS → builds CSSOM → combines into Render Tree → Layout (geometry) → Paint (pixels) → Composite (layers). CSS blocks rendering. JS blocks parsing (unless async/defer). This is why you inline critical CSS and defer everything else.

💡 Interview signal

When asked about CRP, mention that CSS is render-blocking but not parse-blocking, while JS is parse-blocking. This distinction shows deep understanding. Follow up with: "That's why we put CSS in the head and scripts at the bottom (or use defer)."

Web Workers — When to Use

Web Workers run JavaScript in a background thread, separate from the main thread. They can't access the DOM. Communication happens via postMessage. Use them for: heavy computation (sorting large datasets, image processing, crypto), parsing large JSON, or running WebAssembly.

web-worker.tstypescript
// main.ts
const worker = new Worker(new URL('./heavy-task.worker.ts', import.meta.url));
worker.postMessage({ data: largeDataset });
worker.onmessage = (e) => {
  console.log('Result:', e.data); // runs on main thread
};

// heavy-task.worker.ts
self.onmessage = (e) => {
  const result = expensiveComputation(e.data);
  self.postMessage(result); // send back to main thread
};

Trade-off

Workers have overhead: serialization cost for postMessage, no shared memory (unless using SharedArrayBuffer), and no DOM access. Don't use them for trivial tasks — the overhead outweighs the benefit. Use them when computation takes >50ms and would cause visible jank.

📝 Quick Revision

Quick Revision Cheat Sheet

Performance framework: Network → Parse → Render → Interact. Optimize each stage.

Bundle size: Analyze first. Replace heavy libs. Code split by route + component.

CRP: HTML→DOM, CSS→CSSOM, Render Tree→Layout→Paint→Composite. CSS blocks render, JS blocks parse.

Web Workers: Background thread. No DOM. Use for >50ms computation. postMessage for communication.

Core Web Vitals: LCP (loading), INP (interactivity), CLS (visual stability). Measure, don't guess.

Common Interview Questions

Q:How would you optimize a slow React application?

A: First, measure — use React DevTools Profiler and Lighthouse. Then: 1) Reduce bundle size (code split, tree shake, replace heavy deps). 2) Prevent unnecessary re-renders (React.memo, stable refs). 3) Virtualize long lists. 4) Lazy load below-fold content. 5) Optimize images (WebP, srcset, lazy). Always tie improvements to Core Web Vitals metrics.

Q:What's the difference between defer and async on script tags?

A: Both download scripts without blocking HTML parsing. 'async' executes immediately when downloaded (order not guaranteed). 'defer' waits until HTML is fully parsed, then executes in document order. Use defer for scripts that depend on DOM or each other. Use async for independent scripts like analytics.

02

API Handling — Cancel, Race Conditions

Real-world apps make dozens of API calls. The hard part isn't making them — it's handling cancellation, race conditions, retries, and error states gracefully. This is where production code diverges from tutorial code.

🔥 AbortController — Cancelling Requests

AbortController is the standard way to cancel fetch requests. Create a controller, pass its signal to fetch, and call abort() when you need to cancel. This is critical in React — if a component unmounts while a fetch is in-flight, you must cancel it to prevent state updates on unmounted components.

abort-controller.tsxtypescript
// In a React component
useEffect(() => {
  const controller = new AbortController();

  async function fetchData() {
    try {
      const res = await fetch(`/api/search?q=${query}`, {
        signal: controller.signal,
      });
      const data = await res.json();
      setResults(data);
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') {
        // Request was cancelled — this is expected, not an error
        return;
      }
      setError(err); // actual error
    }
  }

  fetchData();
  return () => controller.abort(); // cancel on unmount or query change
}, [query]);

🔥 Race Conditions — The Silent Bug

User types "react" in a search box. Three requests fire: "r", "re", "react". If "re" returns after "react", the UI shows stale results. This is a race condition. Solutions:

  • AbortController — cancel previous request when a new one fires
  • Request ID — track the latest request ID, ignore responses from older IDs
  • Debounce — don't fire until user stops typing (reduces but doesn't eliminate races)
  • Libraries — React Query / SWR handle this automatically with stale-while-revalidate
race-condition-fix.tsxtypescript
// Pattern: ignore stale responses with a flag
useEffect(() => {
  let cancelled = false;

  async function fetchResults() {
    const res = await fetch(`/api/search?q=${query}`);
    const data = await res.json();
    if (!cancelled) {
      setResults(data); // only update if this is still the latest request
    }
  }

  fetchResults();
  return () => { cancelled = true; };
}, [query]);

// Better: combine debounce + AbortController
// Debounce reduces request count, AbortController cancels in-flight ones

fetch vs axios — Real Differences

Featurefetch (native)axios
Built-in✅ No install needed❌ ~13KB dependency
JSON parsingManual: res.json()Automatic
Error handlingOnly rejects on network failure, not 4xx/5xxRejects on any non-2xx status
Interceptors❌ Must wrap manually✅ Request/response interceptors
CancelAbortControllerAbortController (v0.22+) or CancelToken (deprecated)
TimeoutAbortSignal.timeout(5000)Built-in timeout option
ProgressReadableStream (complex)onUploadProgress / onDownloadProgress
Browser supportModern browsers + Node 18+All browsers + Node

💡 Interview answer

"For new projects, I default to fetch — it's native, no dependency, and AbortController covers cancellation. I reach for axios when I need interceptors (auth token injection, global error handling) or upload progress tracking. The key difference most people miss: fetch doesn't reject on HTTP errors — you must check res.ok manually."

📝 Quick Revision

Quick Revision Cheat Sheet

AbortController: Cancel fetch requests. Pass signal to fetch, call abort() on cleanup.

Race conditions: Cancel previous requests OR track request IDs. Debounce helps but doesn't solve.

fetch gotcha: Doesn't reject on 4xx/5xx. Always check res.ok.

axios advantage: Interceptors, auto JSON, rejects on errors. Worth it for complex apps.

03

SSR vs CSR vs Hydration

🔥 VERY IMPORTANT — Asked in every senior interview

Understanding the rendering spectrum (CSR → SSR → SSG → ISR) and how hydration connects them is non-negotiable for senior frontend roles. This is where Next.js, Remix, and modern frameworks live.

The Rendering Spectrum

StrategyHow it worksProsConsUse case
CSRBrowser downloads empty HTML + JS bundle. JS renders everything.Simple deploy, rich interactionsSlow initial load, poor SEO, blank screen flashDashboards, internal tools, SPAs
SSRServer renders full HTML per request. Browser receives ready-to-display page.Fast FCP, great SEO, dynamic dataServer load per request, TTFB depends on server speedE-commerce, news, personalized content
SSGHTML generated at build time. Served as static files.Fastest possible load, CDN-cacheableStale data, rebuild needed for changesBlogs, docs, marketing pages
ISRSSG + revalidation. Serves stale page, regenerates in background.Fast + fresh data, no full rebuildSlight staleness window, Next.js specificProduct pages, content that changes hourly

🔥 Hydration — The Bridge Between Server and Client

Hydration is the process where React takes the server-rendered HTML and "attaches" event listeners and state to make it interactive. The server sends static HTML (fast to display), then the client-side JS "hydrates" it into a fully interactive React app.

The critical insight: during hydration, React doesn't re-render from scratch. It walks the existing DOM and attaches handlers. If the server HTML doesn't match what React expects on the client, you get a hydration mismatch error.

hydration-mismatch.tsxtypescript
// ❌ Common hydration mismatch — using Date or Math.random
function Greeting() {
  // Server renders "Good morning" at 9am server time
  // Client hydrates at 2pm user time → "Good afternoon"
  // React: "Hydration mismatch!"
  const hour = new Date().getHours();
  return <p>{hour < 12 ? 'Good morning' : 'Good afternoon'}</p>;
}

// ✅ Fix: use useEffect for client-only values
function Greeting() {
  const [greeting, setGreeting] = useState('Hello');
  useEffect(() => {
    const hour = new Date().getHours();
    setGreeting(hour < 12 ? 'Good morning' : 'Good afternoon');
  }, []);
  return <p>{greeting}</p>;
}

Dehydration & Rehydration (Data)

Beyond DOM hydration, there's data hydration. Libraries like React Query support "dehydrating" the cache on the server (serializing fetched data into the HTML) and "rehydrating" it on the client (restoring the cache without re-fetching). This avoids the double-fetch problem: server fetches data → renders HTML → client hydrates → client fetches same data again.

💡 Follow-up interviewers ask

"What happens if hydration fails?" — React 18 will attempt to recover by client-rendering the mismatched subtree. In React 17, it was a hard error. Also: "How does Selective Hydration work?" — React 18 can hydrate parts of the page independently, prioritizing the section the user is interacting with.

📝 Quick Revision

Quick Revision Cheat Sheet

CSR: JS renders everything. Fast interactions, slow initial load, poor SEO.

SSR: Server renders HTML per request. Fast FCP, good SEO, server cost.

SSG: Build-time HTML. Fastest load. Stale data.

ISR: SSG + background revalidation. Best of both worlds.

Hydration: React attaches interactivity to server HTML. Mismatch = error.

Data dehydration: Serialize server cache into HTML. Client restores without re-fetching.

Q:When would you choose SSR over SSG?

A: SSR when data changes per request or is personalized (user dashboards, search results). SSG when content is the same for all users and changes infrequently (blog posts, docs). ISR is the middle ground — static pages that revalidate on a schedule.

Q:What causes hydration mismatches and how do you fix them?

A: Mismatches happen when server HTML differs from what React renders on the client. Common causes: Date/time, Math.random(), browser-only APIs (window, localStorage), conditional rendering based on client state. Fix: move client-only logic into useEffect, use suppressHydrationWarning for intentional differences, or use the 'use client' boundary.

04

WebSockets & Real-Time

HTTP is request-response: client asks, server answers, connection closes. WebSocket is a persistent, full-duplex connection — both sides can send data at any time. This is the foundation of real-time features.

How WebSocket Works

It starts as an HTTP request with an Upgrade: websocket header. If the server agrees, the connection upgrades to WebSocket protocol (ws:// or wss://). From there, both client and server can push messages without the overhead of HTTP headers on every message.

websocket-basic.tstypescript
const ws = new WebSocket('wss://api.example.com/live');

ws.onopen = () => {
  console.log('Connected');
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateUI(data); // real-time update
};

ws.onclose = (event) => {
  if (!event.wasClean) {
    // Reconnect with exponential backoff
    setTimeout(() => reconnect(), getBackoffDelay());
  }
};

ws.onerror = (error) => console.error('WebSocket error:', error);

When to Use WebSocket vs Alternatives

TechniqueDirectionUse caseOverhead
WebSocketBidirectionalChat, gaming, collaborative editing, live tradingLow (persistent connection)
SSE (Server-Sent Events)Server → Client onlyLive feeds, notifications, stock tickersVery low (uses HTTP)
Long PollingSimulated real-timeFallback when WebSocket isn't availableHigh (repeated HTTP requests)
Short PollingClient → ServerSimple status checks, low-frequency updatesHighest (constant requests)

💡 Decision framework

Need bidirectional? → WebSocket. Server-only push? → SSE (simpler, auto-reconnect, works with HTTP/2). Can't use either? → Long polling as fallback. Most "real-time" features (notifications, feeds) only need SSE. WebSocket is overkill unless the client also sends frequent messages.

📝 Quick Revision

Quick Revision Cheat Sheet

WebSocket: Persistent, bidirectional. Starts as HTTP upgrade. Low overhead per message.

SSE: Server → client only. Auto-reconnect. Uses HTTP. Simpler than WebSocket.

When WebSocket: Chat, gaming, collab editing — when client sends frequent messages too.

Reconnection: Always implement reconnect with exponential backoff. Connections drop.

05

Service Workers & Offline

A Service Worker is a script that runs in the background, separate from the web page. It acts as a programmable network proxy — intercepting requests, caching responses, and enabling offline functionality. It's the backbone of Progressive Web Apps (PWAs).

Lifecycle

Register → Install (cache assets) → Activate (clean old caches) → Fetch (intercept requests). The SW doesn't control the page on first visit — only on subsequent navigations. It runs on a separate thread, can't access the DOM, and communicates via postMessage.

Caching Strategies

StrategyHow it worksBest for
Cache FirstCheck cache → if miss, fetch from networkStatic assets (CSS, JS, images)
Network FirstTry network → if fail, fall back to cacheAPI responses, dynamic content
Stale While RevalidateServe from cache immediately, fetch update in backgroundContent that can be slightly stale (feeds, profiles)
Network OnlyAlways fetch from network, no cachingReal-time data, auth endpoints
Cache OnlyOnly serve from cacheOffline-first apps, pre-cached assets
service-worker.tstypescript
// Stale-while-revalidate strategy
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open('dynamic-v1').then(async (cache) => {
      const cachedResponse = await cache.match(event.request);
      const fetchPromise = fetch(event.request).then((networkResponse) => {
        cache.put(event.request, networkResponse.clone());
        return networkResponse;
      });
      return cachedResponse || fetchPromise; // serve stale, update in background
    })
  );
});

Why interviewers ask this

Service Workers show you understand the full web platform beyond React. They're critical for: offline support, push notifications, background sync, and performance (serving cached assets instantly). Knowing SW signals you can build production-grade web apps, not just SPAs.

📝 Quick Revision

Quick Revision Cheat Sheet

Service Worker: Background script. Network proxy. Enables offline + caching + push notifications.

Lifecycle: Register → Install → Activate → Fetch. Doesn't control first visit.

Cache First: For static assets. Network First for dynamic data.

Stale While Revalidate: Best of both: instant response + background update.

Limitations: No DOM access. HTTPS only. Separate thread.

06

Security — XSS & CSRF (Frontend)

Security isn't just a backend concern. Frontend devs are the first line of defense against XSS and play a key role in CSRF prevention. Interviewers expect you to know the attack vectors and practical mitigations.

🔥 XSS (Cross-Site Scripting)

Attacker injects malicious script into your page. It runs in the context of your domain — can steal cookies, tokens, user data. Three types: Stored (persisted in DB), Reflected (in URL params), DOM-based (client-side JS manipulation).

Frontend Prevention:

  • Never use dangerouslySetInnerHTML with user input (React escapes by default — don't bypass it)
  • Sanitize HTML if you must render it (DOMPurify library)
  • Use Content Security Policy (CSP) headers to restrict script sources
  • Validate and encode user input on both client and server
  • Use httpOnly cookies for tokens (JS can't access them)

CSRF (Cross-Site Request Forgery)

Attacker tricks the user's browser into making a request to your site (where the user is authenticated). The browser automatically sends cookies, so the request looks legitimate. Example: a hidden form on evil.com that POSTs to your-bank.com/transfer.

Frontend Prevention:

  • CSRF tokens — server generates a unique token per session, frontend sends it with every state-changing request
  • SameSite=Strict or Lax on cookies — prevents cross-origin cookie sending
  • Check Origin / Referer headers on the server
  • Use custom headers (e.g., X-Requested-With) — simple requests can't set custom headers

📝 Quick Revision

Quick Revision Cheat Sheet

XSS: Injected scripts. React escapes by default. Never use dangerouslySetInnerHTML with user input.

CSRF: Forged requests using user's cookies. Use CSRF tokens + SameSite cookies.

CSP: Content-Security-Policy header. Whitelist allowed script/style sources.

Token storage: httpOnly cookie > localStorage. JS can't read httpOnly cookies.

07

React Advanced Internals & Patterns

🔥 This section separates mid-level from senior

Basic React (hooks, state, props) is table stakes. Senior interviews go deeper: reconciliation internals, concurrent features, error boundaries, and when NOT to use popular patterns.

🔥 Reconciliation — Deep Understanding

When state changes, React creates a new virtual DOM tree and diffs it against the previous one. The diffing algorithm (reconciliation) uses two heuristics: 1) Different element types produce different trees (full remount). 2) Keys identify which children are stable across renders. React then computes the minimal set of DOM operations needed.

The Fiber architecture (React 16+) made reconciliation interruptible. Instead of processing the entire tree synchronously, React breaks work into units (fibers) and can pause, prioritize, or abort work. This is the foundation of concurrent features.

React 16 vs React 18 — What Changed

FeatureReact 16/17React 18
RenderingSynchronous (blocking)Concurrent (interruptible)
BatchingOnly in React event handlersAutomatic batching everywhere (setTimeout, promises, native events)
SuspenseCode splitting only (React.lazy)Data fetching + streaming SSR + selective hydration
TransitionsstartTransition — mark updates as non-urgent
HydrationAll-or-nothingSelective hydration — hydrate interactive parts first
Strict ModeDouble-invokes effects in devDouble-invokes effects + simulates unmount/remount

State Scheduling & Concurrent Features

React 18 introduced the concept of update priorities. Not all state updates are equal — typing in an input is urgent, filtering a large list is not. startTransition lets you mark non-urgent updates so React can keep the UI responsive.

transitions.tsxtypescript
import { useState, useTransition } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setQuery(e.target.value); // urgent — update input immediately

    startTransition(() => {
      setResults(filterLargeList(e.target.value)); // non-urgent — can be interrupted
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultsList results={results} />
    </>
  );
}

Stale Closures — The Hidden Bug

A stale closure happens when a function captures an old value of state or props and doesn't see updates. This is the most common source of subtle bugs in React hooks.

stale-closure.tsxtypescript
// ❌ Stale closure — count is always 0 inside the interval
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // always 0 — captured at mount time
      setCount(count + 1); // always sets to 1
    }, 1000);
    return () => clearInterval(id);
  }, []); // empty deps = closure captures initial count

  return <p>{count}</p>;
}

// ✅ Fix 1: Use functional updater
setCount(prev => prev + 1); // doesn't depend on closure value

// ✅ Fix 2: Use ref for latest value
const countRef = useRef(count);
countRef.current = count; // always up to date
// Inside interval: console.log(countRef.current);

Context API — When NOT to Use It

Context is great for low-frequency updates (theme, locale, auth status). It's terrible for high-frequency updates (form state, animation values, frequently changing data) because every consumer re-renders when the context value changes — there's no way to subscribe to a slice of context.

  • ✅ Use Context for: theme, locale, auth, feature flags
  • ❌ Avoid Context for: form state, frequently updating data, large state objects
  • Alternative: Zustand, Jotai (atomic state), or React Query (server state)
  • Workaround: split context into multiple smaller contexts to reduce re-renders

Redux Toolkit — Why It Exists

Classic Redux had too much boilerplate: action types, action creators, reducers, immutable updates. Redux Toolkit (RTK) is the official, opinionated way to write Redux. It uses Immer for immutable updates (write "mutating" code that's actually immutable), createSlice for reducers + actions in one place, and RTK Query for data fetching with caching.

💡 Interview framing

"I use Context for simple, low-frequency global state. For complex client state with many consumers, I reach for Zustand (simpler) or Redux Toolkit (when the team already uses Redux or needs middleware/devtools). For server state, React Query — it handles caching, revalidation, and race conditions out of the box."

Error Boundaries

Error Boundaries are class components that catch JavaScript errors in their child component tree during rendering, lifecycle methods, and constructors. They display a fallback UI instead of crashing the entire app.

error-boundary.tsxtypescript
class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    logErrorToService(error, info.componentStack); // send to Sentry/DataDog
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

// Usage: wrap sections, not the entire app
<ErrorBoundary fallback={<p>Something went wrong in this section.</p>}>
  <RiskyComponent />
</ErrorBoundary>

Limitations: Error Boundaries do NOT catch errors in event handlers (use try/catch), async code (use .catch()), SSR, or errors in the boundary itself. For event handlers, use a global error handler or try/catch within the handler.

Suspense — Beyond Lazy Loading

Suspense lets you declaratively handle loading states. In React 18, it works with: code splitting (React.lazy), data fetching (via compatible libraries like React Query, Relay), and streaming SSR. The component "suspends" by throwing a Promise — React catches it and shows the fallback until the Promise resolves.

suspense.tsxtypescript
import { Suspense, lazy } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart /> {/* loads only when rendered */}
      </Suspense>
    </div>
  );
}

// With streaming SSR (Next.js App Router):
// Server streams the shell immediately, then streams
// the Suspense content when it's ready. User sees
// progressive loading instead of a blank page.

Loaders & Actions (React Router Data APIs)

React Router v6.4+ introduced loaders (fetch data before rendering a route) and actions (handle form submissions). This moves data fetching from components to the route level — similar to how Remix/Next.js work. Benefits: no loading spinners (data is ready when the route renders), parallel data fetching, and automatic revalidation after mutations.

📝 Quick Revision

Quick Revision Cheat Sheet

Reconciliation: Diff virtual DOM trees. Fiber makes it interruptible. Keys identify stable children.

React 18: Concurrent rendering, auto batching, startTransition, selective hydration, streaming SSR.

Stale closures: Hooks capture values at render time. Fix: functional updater or useRef.

Context: Good for low-frequency updates. Bad for forms/animations. All consumers re-render.

Error Boundaries: Class components. Catch render errors. Don't catch event handlers or async.

Suspense: Declarative loading states. Works with lazy, data fetching, streaming SSR.

Common Interview Questions

Q:How does React's reconciliation algorithm work?

A: React diffs the new virtual DOM against the previous one using two heuristics: different element types = full remount, and keys identify stable children in lists. The Fiber architecture breaks this into interruptible units of work, allowing React to pause and prioritize updates. This is what enables concurrent features in React 18.

Q:What are stale closures and how do you prevent them?

A: A stale closure captures an old value of state/props and doesn't see updates. Common in useEffect with empty deps + setInterval. Fix: use functional state updaters (setCount(prev => prev + 1)) which don't depend on the closure value, or use useRef to always have the latest value.

Q:When would you NOT use Context API?

A: Avoid Context for high-frequency updates (form inputs, animations, frequently changing data). Every consumer re-renders when context changes — there's no selector mechanism. Use Zustand/Jotai for fine-grained subscriptions, or React Query for server state. Context is ideal for theme, locale, and auth — things that change rarely.

08

JavaScript — Advanced Practical

These aren't textbook JS questions — they're the kind of problems you hit in production. Interviewers use them to test whether you've actually built things or just read about them.

🔥 Implement Throttle (With Edge Cases)

Throttle ensures a function fires at most once every N milliseconds. The tricky part: should it fire on the leading edge (immediately), trailing edge (after the delay), or both? Most interviewers expect the leading + trailing version.

throttle.tstypescript
function throttle<T extends (...args: unknown[]) => void>(
  fn: T,
  limit: number
): (...args: Parameters<T>) => void {
  let lastCall = 0;
  let timer: ReturnType<typeof setTimeout> | null = null;

  return (...args: Parameters<T>) => {
    const now = Date.now();
    const remaining = limit - (now - lastCall);

    if (remaining <= 0) {
      // Leading edge: enough time has passed, fire immediately
      if (timer) { clearTimeout(timer); timer = null; }
      lastCall = now;
      fn(...args);
    } else if (!timer) {
      // Trailing edge: schedule the last call
      timer = setTimeout(() => {
        lastCall = Date.now();
        timer = null;
        fn(...args);
      }, remaining);
    }
  };
}

// Edge cases to mention in interviews:
// 1. What if the function is called exactly at the limit boundary?
// 2. Should the trailing call use the latest args or the first args?
// 3. Do you need a cancel() method? (yes, for cleanup)
// 4. What about 'this' context? (use .apply or arrow functions)

Memory Leaks in Frontend Apps

Memory leaks happen when the app holds references to objects that are no longer needed, preventing garbage collection. In frontend apps, the most common causes:

  • Forgotten event listeners — added in useEffect without cleanup
  • Uncleared timers — setInterval/setTimeout without clearInterval/clearTimeout
  • Detached DOM nodes — removed from DOM but still referenced in JS
  • Closures holding large objects — callbacks that capture entire scope
  • Uncancelled fetch requests — updating state on unmounted components
  • Global variables / window properties — never garbage collected
memory-leak-fixes.tsxtypescript
// ❌ Memory leak: event listener never removed
useEffect(() => {
  window.addEventListener('resize', handleResize);
  // missing cleanup!
}, []);

// ✅ Fix: always clean up
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

// ❌ Memory leak: interval never cleared
useEffect(() => {
  setInterval(pollServer, 5000);
}, []);

// ✅ Fix: clear on unmount
useEffect(() => {
  const id = setInterval(pollServer, 5000);
  return () => clearInterval(id);
}, []);

// Debugging: Chrome DevTools → Memory tab → Heap Snapshot
// Take snapshot before and after navigation, compare retained objects

📝 Quick Revision

Quick Revision Cheat Sheet

Throttle: Fire at most once per interval. Leading + trailing edges. Remember cancel().

Memory leaks: Event listeners, timers, detached DOM, closures, uncancelled fetches.

Debugging leaks: Chrome DevTools → Memory → Heap Snapshot. Compare before/after.

React cleanup: Always return cleanup function from useEffect. Cancel fetches, clear timers, remove listeners.

09

Architecture & Decision Making

Architecture questions test your ability to make trade-off decisions at scale. There's no single right answer — the interviewer wants to see your reasoning process.

GraphQL vs REST — When to Use What

AspectRESTGraphQL
Data fetchingFixed endpoints, fixed response shapeClient specifies exactly what it needs
Over-fetchingCommon — endpoint returns everythingEliminated — query only requested fields
Under-fetchingMultiple requests for related dataSingle query can traverse relationships
CachingHTTP caching works naturally (GET + URL)Complex — need normalized cache (Apollo)
Learning curveLow — everyone knows RESTHigher — schema, resolvers, query language
ToolingMature, universalGrowing — Apollo, Relay, urql
Best forSimple CRUD, public APIs, microservicesComplex UIs, mobile (bandwidth), multiple consumers

💡 Decision framework

Use REST when: simple CRUD, public API, team is small, caching is critical. Use GraphQL when: complex UI with nested data, multiple client types (web + mobile), over-fetching is a real problem, or you need a BFF (Backend for Frontend) layer.

How to Choose Libraries — The Real Framework

"Why did you pick X over Y?" is a senior-level question. Here's the decision framework that impresses interviewers:

  • Bundle size — Day.js (2KB) vs Moment.js (70KB). Does the size justify the features?
  • Maintenance — Is it actively maintained? Check GitHub: last commit, open issues, release frequency
  • Tree-shakeable — Can you import only what you need? (lodash-es vs lodash)
  • TypeScript support — First-class types or @types package? Quality of types?
  • Community & ecosystem — Docs quality, Stack Overflow answers, plugins
  • API surface — Does it do too much? Simpler libraries are easier to replace
  • Team familiarity — The best library is the one your team can use effectively

Folder Structure for Large-Scale React Apps

folder-structure.txtbash
# Feature-based (recommended for large apps)
src/
├── features/
│   ├── auth/
│   │   ├── components/    # LoginForm, SignupForm
│   │   ├── hooks/         # useAuth, useSession
│   │   ├── services/      # authApi.ts
│   │   ├── types/         # User, Session
│   │   └── index.ts       # public API of this feature
│   ├── dashboard/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── ...
│   └── payments/
├── shared/                # truly shared utilities
│   ├── components/        # Button, Modal, Input
│   ├── hooks/             # useDebounce, useMediaQuery
│   └── utils/             # formatDate, cn()
├── app/                   # routing, layouts
└── config/                # env, constants

# Key principles:
# 1. Co-locate related code (component + hook + test together)
# 2. Features don't import from other features (use shared/)
# 3. Each feature has a public API (index.ts barrel export)
# 4. Flat > nestedavoid more than 3 levels deep

Scalable Frontend Architecture — How to Think About It

When asked "how would you design a scalable frontend," structure your answer around these pillars:

1. Code Organization

  • Feature-based folder structure with clear boundaries
  • Shared component library with consistent API
  • Strict import rules (no circular dependencies)

2. State Management

  • Server state (React Query) vs client state (Zustand) — separate concerns
  • Co-locate state as close to where it's used as possible
  • Avoid global state for everything — most state is local

3. Performance

  • Route-based code splitting (automatic in Next.js)
  • Bundle analysis as part of CI (fail build if bundle exceeds threshold)
  • Performance budgets for Core Web Vitals

4. Developer Experience

  • TypeScript for type safety across the codebase
  • ESLint + Prettier for consistent code style
  • Storybook for component development in isolation
  • CI/CD pipeline: lint → type-check → test → build → deploy

📝 Quick Revision

Quick Revision Cheat Sheet

GraphQL vs REST: REST for simple CRUD + caching. GraphQL for complex UIs + multiple consumers.

Library choice: Bundle size, maintenance, tree-shaking, TS support, team familiarity.

Folder structure: Feature-based. Co-locate. Features don't cross-import. Flat > nested.

Architecture pillars: Code org, state management, performance, DX. Address all four.

10

Tooling & Build Systems

🔥 VERY IMPORTANT — Often overlooked in prep

Most candidates can't explain how their code goes from JSX to a browser-ready bundle. Understanding the build pipeline signals you're a production-ready engineer, not just a component builder.

🔥 Webpack — How Bundling Works Internally

Webpack starts from an entry point (e.g., index.tsx), follows all import/require statements to build a dependency graph, then bundles everything into one or more output files. Along the way, loaders transform files (Babel for JSX → JS, css-loader for CSS, file-loader for images) and plugins handle optimization (minification, code splitting, HTML generation).

webpack-flow.txtbash
# Webpack internal pipeline:
Entry Point (index.tsx)
Resolve imports (build dependency graph)
Apply Loaders (transform files)
babel-loader: JSX/TSJS
css-loader: CSSJS module
file-loader: imageshashed URLs
Apply Plugins (optimize)
TerserPlugin: minify JS
MiniCssExtractPlugin: extract CSS to files
HtmlWebpackPlugin: generate HTML
Code Splitting (dynamic importsseparate chunks)
Output (dist/bundle.[hash].js)

# Key concepts:
# - Chunks: pieces of the bundle (entry chunk, async chunks)
# - Hash: content-based filename for cache busting
# - Loaders: transform individual files
# - Plugins: operate on the entire compilation

🔥 Tree Shaking — How It Actually Works

Tree shaking eliminates dead code — exports that are imported but never used. It relies on ES module static analysis: because import/export are statically analyzable (unlike require), the bundler can determine at build time which exports are used and remove the rest.

Requirements for tree shaking to work:

  • ES modules (import/export), not CommonJS (require)
  • "sideEffects": false in package.json (tells bundler it's safe to remove unused exports)
  • Production mode (development preserves everything for debugging)
  • No side effects in module scope (top-level code that runs on import defeats tree shaking)
tree-shaking.tstypescript
// ✅ Tree-shakeable — bundler can remove unused exports
export function add(a: number, b: number) { return a + b; }
export function multiply(a: number, b: number) { return a * b; }

// Consumer only imports add → multiply is removed from bundle
import { add } from './math';

// ❌ NOT tree-shakeable — CommonJS
module.exports = { add, multiply };
const { add } = require('./math'); // bundler can't statically analyze

// ❌ Side effect prevents tree shaking
export function add(a, b) { return a + b; }
console.log('math module loaded'); // this runs on import — can't be removed

// Practical impact:
// import _ from 'lodash'        → 70KB (entire library)
// import debounce from 'lodash/debounce' → 2KB (just debounce)
// import { debounce } from 'lodash-es'   → 2KB (tree-shaken)

package-lock.json — Why It Matters

package.json specifies version ranges (^1.2.3 means >=1.2.3 <2.0.0). package-lock.json pins the exact versions installed, including all transitive dependencies. Without it, two developers running npm install could get different versions. Always commit it. Use npm ci in CI (installs exactly what's in the lockfile, faster than npm install).

Source Maps — Debugging in Production

Minified production code is unreadable. Source maps are files that map minified code back to the original source. They let you debug production errors with original file names, line numbers, and variable names. Configure your error tracking tool (Sentry) to upload source maps during build — then delete them from the public server (don't expose source code to users).

GitHub Actions — CI/CD for Frontend

GitHub Actions automate your build pipeline. A workflow is defined in a YAML file (.github/workflows/ci.yml). It runs on events (push, PR) and executes jobs (lint, test, build, deploy).

.github/workflows/ci.ymlyaml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci              # install exact lockfile versions
      - run: npm run lint         # catch code style issues
      - run: npm run type-check   # TypeScript errors
      - run: npm run test -- --run  # run tests (not watch mode)
      - run: npm run build        # ensure production build works

# Why YAML?
# - Human-readable structured data format
# - Used by GitHub Actions, Docker Compose, K8s, CloudFormation
# - Indentation-based (like Python) — spaces, not tabs
# - Supports strings, numbers, booleans, lists, maps

Instrumentation in Next.js

Next.js 13.2+ supports an instrumentation.ts file at the project root. It runs when the Next.js server starts — before any request is handled. Use it for: initializing monitoring tools (OpenTelemetry, Sentry), setting up logging, connecting to databases, or registering custom metrics. It runs once on server startup, not per-request.

Sitemap & robots.txt — SEO Essentials

sitemap.xml tells search engines which pages exist and their priority. robots.txt tells crawlers which pages to index and which to skip. In Next.js, you can generate both programmatically using app/sitemap.ts and app/robots.ts. Always include a sitemap for SEO-critical apps. Use robots.txt to block admin pages, API routes, and staging environments from indexing.

📝 Quick Revision

Quick Revision Cheat Sheet

Webpack: Entry → dependency graph → loaders (transform) → plugins (optimize) → output bundles.

Tree shaking: Removes unused exports. Requires ES modules + sideEffects: false + production mode.

package-lock.json: Pins exact versions. Always commit. Use npm ci in CI.

Source maps: Map minified → original code. Upload to Sentry, don't serve publicly.

GitHub Actions: YAML workflows. lint → type-check → test → build. Runs on push/PR.

Instrumentation: Next.js instrumentation.ts — runs once on server start. For monitoring/logging.

SEO files: sitemap.xml for discovery, robots.txt for access control. Generate in Next.js.

Common Interview Questions

Q:What is tree shaking and why might it not work?

A: Tree shaking removes unused exports from the final bundle. It requires ES modules (import/export), not CommonJS (require). It fails when: the package uses CommonJS, there are side effects in module scope, sideEffects isn't set in package.json, or you're importing the entire library instead of specific functions. Example: 'import _ from lodash' defeats tree shaking — use 'import debounce from lodash/debounce' or lodash-es.

Q:Explain how Webpack bundles your application.

A: Webpack starts from the entry point, recursively follows all imports to build a dependency graph. It applies loaders to transform files (Babel for JSX, css-loader for CSS). Then plugins optimize the output (Terser for minification, code splitting for async chunks). The result is one or more bundles with content-hashed filenames for cache busting.

11

Top 25 Advanced Frontend Questions

These are the questions that come up repeatedly in senior frontend interviews. Each one has a short, crisp answer for quick revision and a deeper explanation in the relevant section above.

Q:1. How do you optimize performance of a large React app?

A: Measure first (Lighthouse, React Profiler). Then: code split by route, tree shake, replace heavy deps, virtualize lists, memoize expensive renders, lazy load images/components, optimize CRP. Tie every change to a Core Web Vital metric.

Q:2. Explain SSR, CSR, and hydration. When would you use each?

A: CSR: JS renders everything (dashboards). SSR: server renders HTML per request (SEO, dynamic content). Hydration: React attaches interactivity to server HTML. Use SSG for static content, ISR for content that changes periodically, SSR for personalized/dynamic pages.

Q:3. How do you cancel API requests and handle race conditions?

A: AbortController to cancel fetch requests. Pass signal to fetch, abort on cleanup/new request. For race conditions: cancel previous requests, or use a request ID to ignore stale responses. Libraries like React Query handle this automatically.

Q:4. What is the Critical Rendering Path?

A: HTML→DOM, CSS→CSSOM, combine into Render Tree, Layout (geometry), Paint (pixels), Composite (layers). CSS blocks rendering, JS blocks parsing. Optimize by inlining critical CSS, deferring scripts, and minimizing render-blocking resources.

Q:5. How does React's reconciliation work under the hood?

A: React diffs virtual DOM trees using two heuristics: different types = remount, keys identify stable children. Fiber architecture makes this interruptible — work is broken into units that can be paused and prioritized. This enables concurrent rendering in React 18.

Q:6. What are stale closures? Give an example.

A: A function captures an old state value and doesn't see updates. Common: setInterval in useEffect with empty deps. The interval callback always sees the initial state. Fix: functional updater (setCount(prev => prev + 1)) or useRef for latest value.

Q:7. When should you NOT use Context API?

A: Avoid for high-frequency updates (forms, animations). All consumers re-render on any context change — no selector mechanism. Use Zustand/Jotai for fine-grained subscriptions, React Query for server state. Context is ideal for theme, locale, auth.

Q:8. What is tree shaking and why might it fail?

A: Removes unused exports at build time. Requires ES modules, sideEffects: false in package.json, production mode. Fails with CommonJS, side effects in module scope, or importing entire libraries instead of specific functions.

Q:9. Explain Webpack's bundling process.

A: Entry point → resolve imports (dependency graph) → loaders transform files (Babel, css-loader) → plugins optimize (Terser, code splitting) → output hashed bundles. Chunks are split by entry points and dynamic imports.

Q:10. How do you reduce bundle size?

A: Analyze with webpack-bundle-analyzer. Replace heavy libs (moment→day.js). Import specific functions (lodash/debounce). Code split by route. Lazy load heavy components. Enable tree shaking. Compress with Brotli. Set performance budgets in CI.

Q:11. What is a Service Worker?

A: Background script that acts as a network proxy. Intercepts requests, caches responses, enables offline. Lifecycle: register→install→activate→fetch. Can't access DOM. HTTPS only. Foundation of PWAs.

Q:12. How do you prevent XSS attacks on the frontend?

A: React escapes by default — don't bypass with dangerouslySetInnerHTML. Sanitize HTML with DOMPurify if needed. Use CSP headers. Store tokens in httpOnly cookies (not localStorage). Validate input on both client and server.

Q:13. WebSocket vs SSE — when to use which?

A: WebSocket: bidirectional, persistent connection. For chat, gaming, collab editing. SSE: server→client only, auto-reconnect, uses HTTP. For notifications, live feeds. SSE is simpler — use WebSocket only when client needs to send frequent messages.

Q:14. What is hydration and what causes mismatches?

A: Hydration: React attaches event listeners to server-rendered HTML. Mismatches happen when server HTML differs from client render — Date/time, Math.random(), browser APIs. Fix: useEffect for client-only values, suppressHydrationWarning for intentional differences.

Q:15. How do Error Boundaries work? What don't they catch?

A: Class components with getDerivedStateFromError + componentDidCatch. Catch render errors in child tree, show fallback UI. Don't catch: event handlers (use try/catch), async code, SSR errors, or errors in the boundary itself.

Q:16. GraphQL vs REST — how do you decide?

A: REST: simple CRUD, public APIs, HTTP caching works naturally. GraphQL: complex UIs with nested data, multiple client types, over-fetching is a problem. REST is simpler to cache and debug. GraphQL is more flexible for consumers.

Q:17. How do you structure a large-scale React app?

A: Feature-based folders. Each feature has components, hooks, services, types, and a public API (index.ts). Features don't cross-import. Shared/ for truly reusable code. Co-locate related files. Flat > nested (max 3 levels).

Q:18. What are Web Workers and when would you use them?

A: Background threads for CPU-heavy work. Can't access DOM. Communicate via postMessage. Use for: heavy computation (>50ms), large data processing, image manipulation. Don't use for trivial tasks — serialization overhead isn't worth it.

Q:19. Implement throttle with leading and trailing edge.

A: Track lastCall timestamp. If enough time passed, fire immediately (leading). Otherwise, schedule a trailing call with setTimeout for the remaining time. Clear pending timer on new leading call. Remember to handle cancel() for cleanup.

Q:20. What is React Suspense and how does it work?

A: Declarative loading states. Component 'suspends' by throwing a Promise. React shows fallback until resolved. Works with: React.lazy (code splitting), data fetching (React Query/Relay), streaming SSR. React 18 enables selective hydration with Suspense boundaries.

Q:21. fetch vs axios — real differences?

A: fetch: native, no dependency, doesn't reject on 4xx/5xx (must check res.ok), manual JSON parsing. axios: auto JSON, rejects on errors, interceptors for auth/logging, upload progress. Use fetch for simple apps, axios when you need interceptors or progress tracking.

Q:22. What is package-lock.json and why commit it?

A: Pins exact dependency versions (including transitive). Without it, npm install can resolve different versions on different machines. Always commit it. Use npm ci in CI — installs exactly what's in the lockfile, faster and deterministic.

Q:23. How do source maps work?

A: Map minified production code back to original source. Enable debugging with real file names and line numbers. Upload to error tracking (Sentry) during build. Don't serve publicly — they expose source code. Generated during build with devtool config.

Q:24. What changed in React 18?

A: Concurrent rendering (interruptible), automatic batching (everywhere, not just event handlers), startTransition (mark non-urgent updates), Suspense for data fetching, selective hydration, streaming SSR, useId hook.

Q:25. How do you handle memory leaks in React?

A: Always clean up in useEffect: remove event listeners, clear timers, abort fetch requests. Watch for: closures holding large objects, detached DOM nodes, global variables. Debug with Chrome DevTools Memory tab — heap snapshots before/after navigation.

12

Common Mistakes (Advanced)

These are the mistakes that experienced developers still make — and the ones interviewers specifically look for. Knowing these shows self-awareness and production experience.

🎯

Optimizing before measuring

Adding React.memo, useMemo, and useCallback everywhere 'just in case.' Premature optimization adds complexity without proven benefit.

Profile first with React DevTools and Lighthouse. Only optimize components that actually cause performance issues. Memoization has a cost — the comparison itself isn't free.

🔄

Putting everything in global state

Using Redux/Context for form inputs, modal visibility, and component-local data. Makes the app harder to reason about and causes unnecessary re-renders.

Co-locate state. Most state is local (useState). Server state belongs in React Query. Only truly global state (theme, auth, locale) goes in Context/Redux.

📦

Ignoring bundle size until it's too late

Importing entire libraries, adding dependencies without checking size, never analyzing the bundle. The app slowly becomes 2MB+ of JS.

Add bundle analysis to CI. Set size budgets. Check bundlephobia.com before adding deps. Import specific functions. Review bundle on every PR.

🧹

Missing useEffect cleanup

Not returning cleanup functions from useEffect. Leads to memory leaks, stale state updates, and 'Can't perform a React state update on an unmounted component' warnings.

Always clean up: remove event listeners, clear timers, abort fetch requests. If you add addEventListener, you need removeEventListener.

🔐

Storing tokens in localStorage

localStorage is accessible to any JS on the page. A single XSS vulnerability exposes all tokens.

Use httpOnly cookies for auth tokens. They're not accessible via JavaScript. Combine with SameSite attribute for CSRF protection.

🏗️

No error boundaries in production

A single component error crashes the entire app. Users see a white screen with no way to recover.

Wrap major sections in Error Boundaries. Show meaningful fallback UI. Log errors to monitoring service (Sentry). Allow users to retry or navigate away.

Not handling loading and error states

Only coding the happy path. No loading indicators, no error messages, no empty states. Users see broken UI when things go wrong.

Every async operation needs three states: loading, success, error. Use Suspense for loading, Error Boundaries for errors. Show skeleton loaders, not spinners.

🔑

Using array index as key in dynamic lists

Using index as key causes React to reuse DOM nodes incorrectly when items are reordered, added, or removed. Leads to subtle bugs with form inputs and animations.

Use a stable, unique identifier (id from data). If no id exists, generate one when the item is created (not during render).

13

How to Answer Architecture Questions

Architecture questions don't have a single right answer. The interviewer is evaluating your thought process, not checking a rubric. Here's the framework that works.

The RADIO Framework for Frontend System Design

R — Requirements

Clarify scope. Ask: "Who are the users? What's the scale? What are the must-have features vs nice-to-have? What's the performance budget? Mobile support?" Spend 2-3 minutes here. It shows maturity.

A — Architecture

Draw the high-level component tree. Identify: page layout, major components, data flow direction, API boundaries. Mention rendering strategy (CSR/SSR/SSG) and why.

D — Data Model

Define the data: API response shapes, client state structure, cache strategy. Separate server state (React Query) from client state (local/global). Show the data flow.

I — Interface (API)

Define component interfaces (props), API contracts, and state management boundaries. Show how components communicate. Mention error handling and loading states.

O — Optimizations

Discuss performance: code splitting, virtualization, caching, lazy loading. Mention accessibility, SEO, and monitoring. This is where you show depth.

Example Scenarios

1

Design a real-time collaborative document editor

How would you handle concurrent edits from multiple users?

Answer: Use WebSocket for real-time sync. Implement OT (Operational Transform) or CRDT for conflict resolution. Optimistic UI updates — apply changes locally, sync with server. Show presence indicators (who's editing where). Use Suspense boundaries for loading states. Consider Yjs or Automerge libraries for CRDT implementation.

2

Design an e-commerce product listing page

How would you handle 10,000 products with filters and sorting?

Answer: SSR for initial load (SEO + fast FCP). Virtualized list for rendering (only visible items in DOM). Server-side filtering/sorting with cursor-based pagination. URL-based filter state (shareable, back-button works). Optimistic filter UI with debounced API calls. Image lazy loading with blur placeholders. Cache filter results with React Query.

3

Design a dashboard with real-time metrics

How would you keep data fresh without overwhelming the server?

Answer: SSE or WebSocket for real-time updates (not polling). Stale-while-revalidate for non-critical data. Different refresh intervals per widget based on data criticality. Web Workers for heavy data transformations. Virtualize charts that are off-screen. Error boundaries per widget so one failure doesn't crash the dashboard.

💡 The #1 signal interviewers look for

Trade-off awareness. Never say "I would use X" without explaining why not Y. "I'd use SSR here because SEO matters and the data is personalized. SSG won't work because the content changes per user. CSR would hurt SEO." This shows you've actually made these decisions in production.

14

Last Day Revision Sheet (Advanced)

📋 Quick-fire revision — advanced topics only

Scan this the night before your interview. Each item is a concept you should be able to explain in 30 seconds or less.

🟡 Performance & Rendering

Quick Revision Cheat Sheet

CRP: HTML→DOM, CSS→CSSOM, Render Tree→Layout→Paint→Composite. CSS blocks render, JS blocks parse.

Code splitting: React.lazy + Suspense. Route-based (automatic in Next.js) + component-based.

Tree shaking: Removes unused exports. ES modules only. sideEffects: false. Production mode.

Web Workers: Background thread. No DOM. postMessage. Use for >50ms computation.

Bundle optimization: Analyze → replace heavy deps → specific imports → code split → compress (Brotli).

Core Web Vitals: LCP (loading <2.5s), INP (interactivity <200ms), CLS (stability <0.1).

🟢 React Internals

Quick Revision Cheat Sheet

Reconciliation: Diff virtual DOM. Fiber = interruptible. Keys identify stable children.

React 18: Concurrent rendering, auto batching, startTransition, selective hydration.

Stale closures: Hooks capture render-time values. Fix: functional updater or useRef.

Error Boundaries: Class components. Catch render errors. Don't catch events/async.

Suspense: Declarative loading. Throws Promise. Works with lazy, data fetching, streaming SSR.

Context pitfall: All consumers re-render. No selectors. Bad for high-frequency updates.

🔵 API & Networking

Quick Revision Cheat Sheet

AbortController: Cancel fetch. Pass signal. Abort on cleanup. Check AbortError in catch.

Race conditions: Cancel previous OR track request ID. Debounce reduces but doesn't eliminate.

WebSocket: Persistent, bidirectional. Starts as HTTP upgrade. For chat, gaming, collab.

SSE: Server→client only. Auto-reconnect. HTTP-based. For notifications, feeds.

Hydration: React attaches interactivity to server HTML. Mismatch = error. Selective in React 18.

fetch vs axios: fetch: native, no reject on 4xx. axios: interceptors, auto JSON, rejects on errors.

🟣 Architecture & Tooling

Quick Revision Cheat Sheet

GraphQL vs REST: REST: simple, cacheable. GraphQL: flexible queries, no over-fetching.

Folder structure: Feature-based. Co-locate. No cross-feature imports. Flat > nested.

Webpack: Entry → dependency graph → loaders → plugins → output. Chunks + hashing.

package-lock.json: Pins exact versions. Always commit. npm ci in CI.

Source maps: Map minified → original. Upload to Sentry. Don't serve publicly.

XSS prevention: React escapes by default. No dangerouslySetInnerHTML with user input. CSP headers.

CSRF prevention: CSRF tokens + SameSite cookies + custom headers.

Service Worker: Network proxy. Cache strategies. Offline support. HTTPS only.

🔥 Must-Remember One-Liners

  • Hydration: Server sends static HTML for speed, client JS makes it interactive. Mismatch = bug.
  • Tree shaking: Dead code elimination. ES modules only. sideEffects: false is the key.
  • Stale closure: useEffect with empty deps + setInterval = captures initial state forever.
  • Context re-renders: Every consumer re-renders. Split contexts or use external state.
  • AbortController: The only correct way to cancel fetch. Always use in useEffect cleanup.
  • Error Boundaries: Only catch render errors. Event handlers need try/catch.
  • Bundle size: Every 100KB JS ≈ 300ms on mid-range mobile. Measure, don't guess.
  • Webpack loaders vs plugins: Loaders transform files. Plugins operate on the compilation.
  • npm ci vs npm install: ci uses lockfile exactly, faster, for CI. install resolves and may update.
  • CSR vs SSR decision: Need SEO? → SSR/SSG. Internal tool? → CSR. Personalized + SEO? → SSR.