Skip to content
Tech Interview Prep home

Top 100 Frontend Engineer Interview Questions and Answers

The questions most likely to actually come up in your Frontend Engineer interview, ranked by likelihood — with detailed, senior-level answers covering what an interviewer is really listening for.

Curated: · Written: · Reviewed:

Reviewed 76Review pending 24
QA-1What is the difference between var, let, and const, and when does it matter?(show answer)

I would start var, let, and const scoping from what the user sees and measures, not from the framework's API.

var is function-scoped and hoisted as undefined, while let and const are block-scoped and sit in a temporal dead zone until their declaration runs, so reading them early throws instead of silently giving undefined. const prevents reassigning the binding, not mutating the object it points at.

Concretely, default to const, use let only where reassignment is genuinely needed, and treat freezing or copying as a separate decision, because a const object's properties can still change.

The reason for that specificity is a failure I have seen: A loop that captured a var counter registered 5 click handlers that all reported index 5, because one binding was shared across every iteration.

The three declarations compared.

DeclarationScopeRead before declarationReassignable
varfunctionundefinedyes
letblockthrowsyes
constblockthrowsno

I would not consider it settled without evidence: Test the loop with let and with var on a fixture of 5 handlers and confirm each reports its own index.

Block scope is the default worth having.

Curated: · Written: · Reviewed:

QA-2Explain closures with an example an interviewer would accept.(show answer)

The first question I ask about closures is which render or interaction it changes.

A closure is a function together with the scope it was created in, so it keeps access to those variables after the outer call has returned. That is what makes private state, factories, and memoization possible without a class.

Concretely, return a function from a factory that reads a captured variable, keep the captured data small, and be explicit about when the closure is recreated, because a new closure per render loses whatever it was remembering.

The reason for that specificity is a failure I have seen: A counter built with a closure inside a component was recreated on every render, so the count reset to 0 on each keystroke and the bug was reported as lost input.

The captured value persists.

CallCaptured count
counter first call1
counter second call2
new factory, first call1

I would not consider it settled without evidence: Call the returned function twice and show the captured value persisting between calls.

A closure keeps the scope alive, not a copy of the value.

Curated: · Written: · Reviewed:

QA-3How is the value of this determined in JavaScript?(show answer)

With this-binding rules, a passing local build is where I start checking rather than stop.

In an ordinary function call, this is set by how the function is called, which is the object before the dot, the new target, or the explicit argument to call, apply, or bind. Arrow functions have no this of their own and take it from the surrounding scope, which is why they suit callbacks.

Concretely, prefer arrow functions for callbacks that need the enclosing this, bind or use class fields for methods passed as handlers, and avoid relying on a global this, which is undefined in modules and strict mode.

The reason for that specificity is a failure I have seen: A class method passed directly as an event handler threw when it read a property of this, because the handler was called with this undefined rather than the component instance.

Four call forms in one fixture.

Call formthis resolves toHandler works
obj.methodobjyes
detached, then calledundefined in modulesno, throws
called with call and objobjyes
arrow inside a methodthe method's thisyes
4 forms tested3 resolve as intended1 needs binding

I would not consider it settled without evidence: Call the method detached from its object and confirm it still resolves this as intended.

Ask how the function is called, not where it was written.

Curated: · Written: · Reviewed:

QA-4How does prototypal inheritance work, and how do classes relate to it?(show answer)

I would answer prototypes and class inheritance by tracing one real interaction through the browser.

Every object has a prototype link, and a property lookup walks that chain until it finds the key or reaches null. Class syntax is shorthand over that chain, so a class method lives on the prototype and is shared by every instance.

Concretely, put shared behaviour on the prototype through class methods, keep per-instance data in fields, and never modify built-in prototypes, which leaks into every library on the page.

The reason for that specificity is a failure I have seen: A polyfill added a method to the array prototype without making it non-enumerable, so every for-in loop in the application began iterating that method, which broke 3 unrelated components.

Where members live, for 1,000 instances.

MemberLives onCopies
class methodprototype1
instance fieldinstance1,000
static methodconstructor1

I would not consider it settled without evidence: Inspect the prototype chain of an instance and confirm shared methods appear once rather than per instance.

Lookups walk a chain, so know what is on it.

Curated: · Written: · Reviewed:

QA-5Explain the event loop. In what order do a promise callback, a zero-delay timeout, and a paint happen?(show answer)

My approach to the event loop and microtasks separates what the engine guarantees from what it usually does.

The engine runs one task to completion, then drains the entire microtask queue, then paints if needed, then takes the next macrotask. Promise callbacks are microtasks, so they run before any timeout even with a zero delay.

Concretely, use microtasks for work that must finish before the next paint, defer heavy work to a macrotask or an idle callback so the frame can render, and never busy-wait, which blocks everything.

The reason for that specificity is a failure I have seen: A recursive promise chain that re-queued itself never let the browser paint, so a spinner froze for 4 seconds while the microtask queue kept draining.

One turn of the loop.

StepQueueOrder
synchronous codecurrent task1
promise callbackmicrotask2
paintrender3
zero-delay timeoutmacrotask4

I would not consider it settled without evidence: Log the order of a promise callback, a zero-delay timeout, and a rendered frame and confirm it matches the model.

Microtasks jump the queue, so keep them short.

Curated: · Written: · Reviewed:

QA-6What does async and await change compared with then chains, and what stays the same?(show answer)

For promises and async await, I would name the failure a user would notice before the technique.

await is syntax over the same promises, so an async function always returns a promise and await suspends only that function rather than the thread. Failures surface as rejections, which is why try and catch work only around an awaited call.

Concretely, await inside try and catch for error paths, start independent work together with a combinator rather than awaiting in sequence, and return promises so callers can compose them.

The reason for that specificity is a failure I have seen: Three independent requests were awaited one after another, which made a page take 1.8 seconds instead of the 620 milliseconds the slowest request needed.

Same three requests, two orderings.

PatternRequestsTotal time
sequential awaits31,800 ms
started together3620 ms

I would not consider it settled without evidence: Measure the sequential and the parallel version and report both timings.

Await in series only when the next call needs the previous result.

Curated: · Written: · Reviewed:

QA-7When would you use all, allSettled, race, or any?(show answer)

I would test promise combinators on a slow device and a real network, not only on a laptop.

all rejects as soon as one input rejects and otherwise resolves with every value, allSettled always resolves with a status per input, race settles with the first result of either kind, and any resolves with the first fulfilment. The choice decides what a partial failure does to the screen.

Concretely, use allSettled when the screen can render partial data, all when every piece is required, race for timeouts, and any for redundant sources, and always show which part failed.

The reason for that specificity is a failure I have seen: A dashboard used all for 6 independent widgets, and one failing request left the whole page blank instead of showing the 5 widgets that had loaded.

One of six requests fails.

CombinatorOutcomeWidgets shown
allrejects0 of 6
allSettledresolves5 of 6
racefirst settles1

I would not consider it settled without evidence: Force one input to reject and confirm the screen renders the remaining data with an error for the failed part.

Pick the combinator that matches what a partial failure should look like.

Curated: · Written: · Reviewed:

QA-8A user types quickly in a search box and results arrive out of order. How do you fix it?(show answer)

The part of cancelling superseded requests that interviewers probe is the edge case, not the syntax.

Responses can resolve in any order, so the last response to arrive is not necessarily the one for the latest query. Cancelling superseded requests, or ignoring responses that no longer match the current input, keeps the screen consistent.

Concretely, create an abort controller per request, abort the previous one when a new query starts, ignore the resulting abort error, and compare a request token with current state before rendering.

The reason for that specificity is a failure I have seen: A search box rendered results for the query typed 3 keystrokes earlier, because a slower earlier response arrived last and overwrote the correct list.

Two queries, responses reversed.

QueryResponse arrivesWithout a guardWith abort
reasecondrendered last, wrongignored
reactfirstoverwrittenrendered

I would not consider it settled without evidence: Delay the first response deliberately and confirm only the latest query's results render.

The latest response is not the latest answer.

Curated: · Written: · Reviewed:

QA-9What is the difference between debounce and throttle, and when do you use each?(show answer)

I would anchor debounce and throttle in a number from the profiler or the field, not in a preference.

Debounce waits until activity stops before running, which suits a search box, and throttle runs at most once per interval during continuous activity, which suits scroll and resize. The wrong choice either delays feedback or floods the handler.

Concretely, debounce text input by roughly 200 to 300 milliseconds, throttle scroll work to once per frame, clear timers on unmount, and keep the trailing call so the last event is not lost.

The reason for that specificity is a failure I have seen: A scroll handler debounced at 300 milliseconds only updated a sticky header after the user stopped scrolling, so the header lagged for the whole gesture.

Three seconds of continuous scrolling.

StrategyHandler runsFeels
none540janky
throttle to one per frame186smooth
debounce 300 ms1laggy

I would not consider it settled without evidence: Count handler runs during a 3-second continuous gesture for both strategies and compare the frame rate.

Debounce for the end of activity, throttle for during it.

Curated: · Written: · Reviewed:

QA-10How do you copy an object in JavaScript, and what goes wrong with the easy answers?(show answer)

What separates a strong answer on copying objects safely is knowing why the naive version breaks.

Spread and assign copy one level, so nested objects stay shared and a mutation inside the copy changes the original. A structured clone deep-copies most data but rejects functions, and a JSON round trip loses dates, undefined, maps, and sets.

Concretely, copy to the depth you actually need, use a structured clone for plain nested data, and prefer immutable updates that replace only the branch that changed.

The reason for that specificity is a failure I have seen: A form reset used a shallow copy of its initial state, and because the nested address object was shared, cancelling the form left the edited address in place for 40 percent of submissions.

What each method preserves.

MethodNested objectsDatesFunctions
spreadsharedkeptkept
JSON round tripcopiedbecome stringslost
structured clonecopiedkeptthrows

I would not consider it settled without evidence: Mutate a nested field in the copy and assert the original is unchanged.

Know how deep your copy goes.

Curated: · Written: · Reviewed:

QA-11When is loose equality acceptable, and what surprises does coercion cause?(show answer)

I would treat equality and type coercion as something every browser, input mode, and locale has to survive.

Loose equality converts operands before comparing, which makes several unrelated values equal, while strict equality compares without conversion. The one common use for the loose form is a null check, since null and undefined are loosely equal to each other and to nothing else.

Concretely, use strict equality everywhere, compare loosely against null to catch both null and undefined, and use the same-value helper when NaN or signed zero matters.

The reason for that specificity is a failure I have seen: A guard compared a count loosely against false, which treated an empty string and 0 as the same state, so a product with 0 items in stock rendered as having no data rather than being out of stock.

Four comparisons.

ComparisonResult
0 loosely equals an empty stringtrue
0 strictly equals an empty stringfalse
null loosely equals undefinedtrue
NaN strictly equals NaNfalse

I would not consider it settled without evidence: Run the comparison table for 0, an empty string, null, and undefined and confirm each case takes the intended branch.

Compare without conversion unless you mean to.

Curated: · Written: · Reviewed:

QA-12Which array methods mutate, and why does that matter in a component?(show answer)

Before changing anything for array methods and reference equality, I would write down how I will know it worked.

push, splice, sort, and reverse change the array in place, while map, filter, slice, concat, and the newer copying sort and reverse return a new array. A mutation keeps the same reference, so a framework comparing by reference sees no change and skips the re-render.

Concretely, treat state arrays as immutable, copy before sorting, and return new arrays from reducers so reference equality reflects a real change.

The reason for that specificity is a failure I have seen: A list was sorted in place inside a component, the reference never changed, and the table kept showing the old order until an unrelated update forced a re-render.

Mutation against a new reference.

OperationMutatesNew reference
pushyesno
sortyesno
spread into a new arraynoyes
copying sortnoyes
4 operations2 mutate in place2 return a copy

I would not consider it settled without evidence: Compare the array reference before and after the update and confirm it changed when the data changed.

New data means a new reference.

Curated: · Written: · Reviewed:

QA-13What is hoisting, and which declarations does it affect?(show answer)

I would start hoisting from what the user sees and measures, not from the framework's API.

Function declarations are hoisted with their body, so they can be called above their line, while var is hoisted as undefined, and let, const, and class are hoisted but unusable until their declaration runs. That last case throws rather than returning undefined.

Concretely, declare before use regardless of the rules, keep module-level side effects out of hoisted functions, and read a temporal dead zone error as a clue to a declaration-order bug.

The reason for that specificity is a failure I have seen: A module called a helper above its arrow-function declaration, which threw a reference error only in production, where the bundler had reordered two imports.

Callable above its own line.

DeclarationCallable earlier
function declarationyes
var holding a functionno, undefined
const arrow functionno, throws
classno, throws
4 declarations1 callable early

I would not consider it settled without evidence: Move each call above its declaration in a test and confirm the behaviour matches the documented rule.

Hoisting explains the error, but order still matters.

Curated: · Written: · Reviewed:

QA-14Which practical differences between ES modules and CommonJS affect a frontend build?(show answer)

The first question I ask about ES modules versus CommonJS is which render or interaction it changes.

ES modules are statically analysable, so a bundler can drop unused exports, and their bindings are live. CommonJS resolves at runtime and cannot be reliably tree-shaken, which is why one CommonJS dependency can keep dead code in the bundle.

Concretely, ship ES modules, avoid dynamic requires, import only the named exports you use, and check the bundle for CommonJS dependencies that defeat tree shaking.

The reason for that specificity is a failure I have seen: Importing a whole utility library through its CommonJS build added 74 kilobytes to the bundle for one function that cost 2 kilobytes from the module entry.

One function, two import styles.

ImportBundle cost
whole CommonJS library74 KB
named module import2 KB

I would not consider it settled without evidence: Compare bundle size with the named import and with the whole-library import.

Static imports are what make tree shaking possible.

Curated: · Written: · Reviewed:

QA-15What is the iterator protocol, and where would you use a generator?(show answer)

With iterators and generators, a passing local build is where I start checking rather than stop.

An iterable exposes a method returning an object with a next function, which is what for-of, spread, and destructuring consume. A generator is the convenient way to write one, and because it can pause, it suits lazy sequences and paging through data.

Concretely, implement the iterator symbol for custom collections, use a generator for lazy or paged production, and remember a generator is consumed once.

The reason for that specificity is a failure I have seen: A paginated fetch was written as a generator that was iterated twice, and the second pass produced 0 items because the generator had already completed.

Three sources compared.

SourceReusableLazy
arrayyesno
generatornoyes
custom iterableyesyes

I would not consider it settled without evidence: Iterate the sequence twice in a test and confirm the behaviour you rely on.

A generator is a one-time sequence unless you recreate it.

Curated: · Written: · Reviewed:

QA-16How do optional chaining and nullish coalescing change defensive code, and where do they hide bugs?(show answer)

I would answer optional chaining and nullish defaults by tracing one real interaction through the browser.

Optional chaining short-circuits on null or undefined and yields undefined, and nullish coalescing supplies a default only for null or undefined rather than for every falsy value. Together they remove long guard chains, but they can also hide a missing field that should have been an error.

Concretely, use them for genuinely optional data, fall back with the logical or operator only when 0 and an empty string should also be replaced, and validate responses at the boundary rather than defaulting deep in a render.

The reason for that specificity is a failure I have seen: A price of 0 was replaced by a fallback of 19.99 because the code used the logical or operator, and 12 free items displayed a price for two days.

A price of zero.

ExpressionValue shown
price or 19.9919.99, wrong
price with a nullish default0, correct

I would not consider it settled without evidence: Test each default with 0, an empty string, null, and undefined and confirm only the intended cases fall back.

Default on missing, not on falsy.

Curated: · Written: · Reviewed:

QA-17Where do destructuring and spread help, and what are their limits?(show answer)

My approach to destructuring and spread limits separates what the engine guarantees from what it usually does.

Destructuring names the fields a function actually uses, which documents the contract, and spread copies one level into a new object or array. Neither validates the shape, so a renamed field silently becomes undefined.

Concretely, destructure props and options with defaults, spread to build new objects rather than mutating, and validate external data before destructuring it deeply.

The reason for that specificity is a failure I have seen: An API renamed a field from snake case to camel case, destructuring produced undefined, and the cart displayed a non-numeric total for 8 hours before anyone noticed.

A renamed field.

Source fieldDestructured asRendered
total_pricetotalPriceundefined
total_pricetotal_price42.00

I would not consider it settled without evidence: Assert the shape of external data at the boundary and fail loudly when a required field is missing.

Destructuring reads the shape, it does not check it.

Curated: · Written: · Reviewed:

QA-18How would you memoize an expensive pure function, and when is it a bad idea?(show answer)

For memoizing an expensive function, I would name the failure a user would notice before the technique.

Memoization trades memory for time by caching results keyed on the arguments, which only holds for pure functions. An unbounded cache on a high-cardinality key is a memory leak, and a cheap function gains nothing.

Concretely, key on a stable serialization of the arguments, bound the cache with a least-recently-used policy, and measure the hit rate before keeping it.

The reason for that specificity is a failure I have seen: A memoized formatter keyed on a whole object grew to 180 megabytes over a long session because every render produced a new key, and the tab crashed on low-memory devices.

One hour of use.

CacheHit rateMemory
unbounded, object key4%180 MB
500 entries, string key71%2 MB

I would not consider it settled without evidence: Measure the cache hit rate and memory growth over a realistic session before shipping the cache.

A cache without a bound and a hit rate is a leak.

Curated: · Written: · Reviewed:

QA-19Why attach one listener to a container instead of one per row?(show answer)

I would test event delegation on a slow device and a real network, not only on a laptop.

Events bubble, so a single listener on an ancestor can handle any descendant by inspecting the event target. Setup cost and memory then stay constant as the list grows, and rows added later work without new listeners.

Concretely, attach one listener on the container, resolve the closest matching element from the target, ignore events from outside the intended area, and handle focus, blur, and scroll separately since they do not bubble the same way.

The reason for that specificity is a failure I have seen: A table attached a click listener to each of 5,000 rows, which made the initial render take 900 milliseconds on a mid-range phone.

Five thousand rows.

ApproachListenersRender time
one per row5,000900 ms
delegated1120 ms

I would not consider it settled without evidence: Count listeners and measure render time for the per-row and the delegated version.

One listener can serve a thousand rows.

Curated: · Written: · Reviewed:

QA-20You have a thousand nodes to update. How do you do it without janking the page?(show answer)

The part of batching DOM reads and writes that interviewers probe is the edge case, not the syntax.

Interleaving reads of layout properties with writes forces the browser to lay out synchronously each time, which is the usual cause of jank in a loop. Batching all reads and then all writes lets it lay out once.

Concretely, take every measurement first, write in a single pass, schedule visual updates in an animation frame, and build detached nodes or a fragment before inserting.

The reason for that specificity is a failure I have seen: A loop that read an element's height and then set a style on each of 1,000 nodes forced 1,000 layouts, took 1.4 seconds, and produced a 700 millisecond long task.

One thousand nodes.

PatternForced layoutsDuration
reads and writes interleaved1,0001,400 ms
batched reads then writes190 ms

I would not consider it settled without evidence: Record a performance profile and confirm the number of forced layouts falls to one.

Read everything, then write everything.

Curated: · Written: · Reviewed:

QA-21What does fetch not do for you that people assume it does?(show answer)

I would anchor the guarantees fetch does not give in a number from the profiler or the field, not in a preference.

fetch rejects only on a network failure, so a 404 or a 500 resolves normally and must be checked explicitly. It also has no timeout, no retry, and sends no cookies cross-origin unless credentials are requested.

Concretely, check the ok flag, read the error from the body, add an abort-based timeout, retry only idempotent requests with backoff, and set credentials explicitly.

The reason for that specificity is a failure I have seen: A client treated every resolved fetch as success, so a 500 response body was parsed as data and the screen showed an empty state instead of an error for 3 weeks.

Three cases fetch leaves to you.

Casefetch behaviourWhat you add
404 or 500resolvescheck the ok flag
slow serverwaits indefinitelyabort after a timeout
cross-origin cookiesomittedrequest credentials

I would not consider it settled without evidence: Test a 500 response and a network timeout and confirm each surfaces a distinct error state.

A resolved fetch is not a successful request.

Curated: · Written: · Reviewed:

QA-22Where would you store a theme preference, an unsent draft, and an auth token?(show answer)

What separates a strong answer on browser storage choices is knowing why the naive version breaks.

Local storage is synchronous and persistent, session storage clears with the tab, and cookies travel with matching requests. A token in local storage is readable by any script on the page, which turns one injection bug into account takeover.

Concretely, keep small preferences in local storage, drafts in the browser database, and auth in a cookie the server marks as script-inaccessible, and wrap every storage read in a try block because private modes throw.

The reason for that specificity is a failure I have seen: An access token kept in local storage was read by a third-party script, and every session had to be invalidated for 30,000 users.

Three values, three stores.

DataStoreReason
theme preferencelocal storagesmall and persistent
unsent draftbrowser databaselarger and asynchronous
auth tokenserver-set cookienot readable by scripts

I would not consider it settled without evidence: Attempt to read the token from the console and confirm application code cannot reach it.

Storage choice is a security decision, not only a convenience.

Curated: · Written: · Reviewed:

QA-23Which TypeScript features earn their keep in a React codebase?(show answer)

I would treat TypeScript that earns its keep as something every browser, input mode, and locale has to survive.

A discriminated union models screen state so impossible combinations cannot compile, and generics keep component and hook APIs reusable without falling back to any. The weak point is trust, because one cast at the API boundary removes every guarantee downstream.

Concretely, model loading, error, and success as one union rather than three booleans, type props explicitly, validate external data with a schema at the boundary, and treat every any as a debt to pay off.

The reason for that specificity is a failure I have seen: Three separate boolean flags allowed loading and error to be true at once, and a screen rendered a spinner on top of an error message in 4 percent of sessions.

Three booleans against one union.

ModelImpossible statesCaught at compile time
three booleans4 of 8 combinationsno
discriminated union0yes

I would not consider it settled without evidence: Model the state as a union and confirm the impossible combination no longer compiles.

Make the wrong state impossible rather than handled.

Curated: · Written: · Reviewed:

QA-24A user in another time zone sees the wrong date on a record. What went wrong?(show answer)

Before changing anything for dates and locales in the browser, I would write down how I will know it worked.

A timestamp rendered in the browser's local zone shifts across midnight, and formatting differs by locale. The formatting tools handle presentation, but which zone defines the business day is a product decision that has to be made explicitly.

Concretely, store timestamps in coordinated universal time, decide whether a value is an instant or a calendar day, format with an explicit zone and locale, and never build a date string by slicing an encoded timestamp.

The reason for that specificity is a failure I have seen: An order placed at 8 pm Eastern showed as the next day for users in Europe, because the interface sliced the encoded timestamp, and support handled 60 tickets in a week.

One timestamp, two readings.

ValueShown in New YorkShown in Berlin
raw instantSep 2Sep 3
business day, EasternSep 2Sep 2

I would not consider it settled without evidence: Render the same timestamp for three zones and confirm each matches the intended business day.

Say whether you are showing an instant or a day.

Curated: · Written: · Reviewed:

QA-25Build an autocomplete input. What does a strong answer include beyond fetching results?(show answer)

I would start building an autocomplete from what the user sees and measures, not from the framework's API.

An autocomplete is a small system with input handling, asynchronous data, caching, and accessibility. Interviewers look for debouncing, out-of-order response handling, keyboard navigation, and an accessible list, not only a working request.

Concretely, debounce the input, abort superseded requests, cache by query, support arrow keys, Enter, and Escape, mark the input as a combobox with an active option, and announce the result count to assistive technology.

The reason for that specificity is a failure I have seen: A shipped autocomplete had no keyboard support, so keyboard and screen-reader users could not select any of the 8 suggestions and it failed an accessibility audit before launch.

What each requirement needs.

RequirementImplementation
fast typingdebounce 250 ms
out-of-order resultsabort the previous request
keyboard usearrows, Enter, Escape
screen readercombobox role and a live result count

I would not consider it settled without evidence: Operate the component with the keyboard alone and with a screen reader and confirm every suggestion is reachable and announced.

An autocomplete is judged on the parts around the fetch.

Curated: · Written: · Reviewed:

QA-26Is the virtual DOM faster than direct DOM manipulation?(show answer)

The first question I ask about the virtual DOM's actual benefit is which render or interaction it changes.

The virtual DOM is not inherently faster than well-written imperative updates. It trades some speed for a model where the screen is a function of state, and the framework batches and diffs so most updates are cheap enough without hand-written DOM code.

Concretely, describe the trade as maintainability plus batching rather than raw speed, and reach for direct DOM work only on a measured hot path such as an animation or a canvas.

The reason for that specificity is a failure I have seen: A team rewrote a virtualized table into manual DOM updates expecting a speedup, gained 6 percent, and added 900 lines that later caused 3 rendering bugs.

The same table, two implementations.

ApproachInteraction timeLines of code
framework render18 ms120
manual DOM17 ms1,020

I would not consider it settled without evidence: Profile both approaches on the same interaction before claiming either is faster.

The virtual DOM buys a model, not magic speed.

Curated: · Written: · Reviewed:

QA-27Why does React need keys, and what breaks when you use the array index?(show answer)

With reconciliation and keys, a passing local build is where I start checking rather than stop.

React matches elements between renders by position and key, so a key says which item is which when the list reorders. An index key claims the item at a position is the same item, so state, focus, and DOM nodes follow the position rather than the data.

Concretely, use a stable id from the data, never the index for lists that reorder or filter, and never generate a fresh key per render, which unmounts and remounts every row.

The reason for that specificity is a failure I have seen: A task list used index keys, and deleting the first of 5 rows moved the typed text in each input up one row, so a user's note ended up on the wrong task.

Deleting the first row.

KeyRows after deleteInput text
array indexshift upfollows the position, wrong
item idshift upstays with its item

I would not consider it settled without evidence: Reorder the list while each row holds local state and confirm the state stays with its item.

A key identifies the data, not the slot.

Curated: · Written: · Reviewed:

QA-28Why does reading state right after setting it show the old value?(show answer)

I would answer state updates and batching by tracing one real interaction through the browser.

A state setter schedules an update rather than assigning, and updates from the same event are batched before a re-render. The variable in scope belongs to the render that is still running, so it keeps the old value.

Concretely, use the updater form when the next value depends on the previous one, read the new value in the next render or an effect, and avoid deriving state from state when it can be computed while rendering.

The reason for that specificity is a failure I have seen: Two increments in one handler both read the count as 0 and set it to 1, so a quantity advanced by 1 instead of 2 and lost updates on fast clicks.

Two calls in one handler, starting at zero.

FormCallsFinal count
set to count plus one21
updater from the previous value22

I would not consider it settled without evidence: Call the setter twice in one handler and assert the final value matches the updater form.

Setters schedule, so pass the previous value.

Curated: · Written: · Reviewed:

QA-29How do you decide what goes in an effect's dependency list?(show answer)

My approach to effect dependencies separates what the engine guarantees from what it usually does.

An effect should list every value from render that it reads, so it re-runs when those change. Omitting one freezes the effect on the first render's values, and including an unstable object or function re-runs it on every render.

Concretely, list everything the effect reads, stabilize functions or move them inside the effect, keep values that should not trigger a re-run in a ref, and question whether the work belongs in an effect at all.

The reason for that specificity is a failure I have seen: A subscription effect omitted the room id, so switching chat rooms kept the old connection and every message arrived on the wrong screen.

Switching rooms.

DependenciesBehaviour
empty listkeeps the first room's connection
the room idcloses the old, opens the new
none declaredreconnects on every render
3 configurations1 correct

I would not consider it settled without evidence: Change each dependency in a test and confirm the effect re-runs and cleans up as intended.

Everything the effect reads is a dependency.

Curated: · Written: · Reviewed:

QA-30What must an effect clean up, and what happens if it does not?(show answer)

For effect cleanup, I would name the failure a user would notice before the technique.

Anything an effect starts that outlives the render must be stopped in its cleanup, including subscriptions, timers, listeners, and in-flight requests. Without cleanup the page leaks memory and updates components that are gone.

Concretely, return a cleanup that removes listeners, clears timers, aborts requests, and closes connections, and remember it also runs between re-runs rather than only on unmount.

The reason for that specificity is a failure I have seen: An interval left running after unmount kept firing every second, and after a user visited 20 pages the tab held 20 intervals and grew by 60 megabytes.

What each start needs.

Started in the effectCleanup
event listenerremove it
intervalclear it
requestabort it
subscriptionunsubscribe

I would not consider it settled without evidence: Mount and unmount the component 50 times in a test and confirm listener and timer counts return to their starting values.

If the effect starts it, the cleanup stops it.

Curated: · Written: · Reviewed:

QA-31When are the memoization hooks worth using?(show answer)

I would test when memoization hooks pay off on a slow device and a real network, not only on a laptop.

They trade a little memory and complexity for a stable reference or a skipped computation. They pay off when the computation is genuinely expensive or the value feeds a memoized child or an effect, and they cost more than they save around trivial work.

Concretely, profile first, memoize the expensive computation or the prop passed to a memoized child, keep dependency lists honest, and delete memoization that no longer has a measured reason.

The reason for that specificity is a failure I have seen: A component wrapped 40 cheap values in memoization hooks, which added 3 milliseconds of overhead per render and made the interaction slower than the version with none.

Three candidates.

CaseMemoizeReason
filtering 50,000 rowsyesgenuinely expensive
object prop to a memoized childyesreference stability
joining two stringsnocheaper than the hook

I would not consider it settled without evidence: Profile the interaction with and without the memoization and keep only what measurably helps.

Memoize for a measured reason, not by habit.

Curated: · Written: · Reviewed:

QA-32A component re-renders more than you expect. How do you find out why?(show answer)

The part of diagnosing extra re-renders that interviewers probe is the edge case, not the syntax.

A component re-renders when its own state or context changes, when its parent re-renders, or when its props differ by reference. Memoizing a component only helps when its props are referentially stable, which usually means the parent must memoize them too.

Concretely, use the profiler to find what triggered the render, stabilize object and function props, split context so unrelated consumers stay asleep, and move state down to the component that owns it.

The reason for that specificity is a failure I have seen: A new inline style object passed to a memoized list defeated the memoization, so all 800 rows re-rendered on every keystroke and typing dropped to 12 frames per second.

Eight hundred rows, one keystroke.

PropRows re-renderedFrame rate
inline object80012 fps
memoized object160 fps

I would not consider it settled without evidence: Record a profile and confirm the render count per keystroke falls to the components that actually changed.

Memoization without stable props changes nothing.

Curated: · Written: · Reviewed:

QA-33What is the cost of putting everything in one context?(show answer)

I would anchor context and broadcast re-renders in a number from the profiler or the field, not in a preference.

Every consumer re-renders when any part of a context value changes, because the value is compared by reference. One large context therefore wakes unrelated parts of the tree on every small update.

Concretely, split context by update frequency, keep stable data apart from fast-changing data, memoize the provider value, and consider a store with selectors when many components read slices of the same data.

The reason for that specificity is a failure I have seen: One context held both the theme and the live cart, so every cart update re-rendered 120 components including the static navigation, and interaction latency rose to 180 milliseconds.

Components woken per cart update.

DesignComponents re-rendered
one combined context120
theme and cart split6

I would not consider it settled without evidence: Count re-renders per update before and after splitting the context.

Context is a broadcast, so split it by what changes.

Curated: · Written: · Reviewed:

QA-34How do you decide whether state belongs local, lifted, or global?(show answer)

What separates a strong answer on where state should live is knowing why the naive version breaks.

State belongs at the lowest common owner of the components that need it. Lifting it higher re-renders unrelated subtrees and makes the component harder to reuse, while duplicating it in two places guarantees the copies drift apart.

Concretely, keep state local until a second component needs it, lift to the nearest common parent then, derive rather than duplicate, and reserve a store for genuinely application-wide values such as session or theme.

The reason for that specificity is a failure I have seen: Form state was hoisted to a page component so a sibling could read one field, and every keystroke re-rendered a 40-component tree, dropping typing to 20 frames per second.

One keystroke.

PlacementComponents re-renderedTyping
page level4020 fps
local to the field160 fps

I would not consider it settled without evidence: Measure re-render counts and typing latency after moving the state to its owner.

Put state where it is used, not where it is convenient.

Curated: · Written: · Reviewed:

QA-35When would you use a controlled input, and when an uncontrolled one?(show answer)

I would treat controlled and uncontrolled inputs as something every browser, input mode, and locale has to survive.

A controlled input keeps its value in state, which makes validation, formatting, and cross-field logic straightforward at the cost of a render per keystroke. An uncontrolled input leaves the value in the DOM and is read on submit, which is cheaper and fine for simple forms.

Concretely, control the fields whose value drives other parts of the screen, leave large simple forms uncontrolled and read them on submit, and never switch a field between the two, which loses its value.

The reason for that specificity is a failure I have seen: A 60-field form used controlled inputs with all state at the top, so each keystroke re-rendered the whole form and produced 140 milliseconds of input delay on a mid-range phone.

Sixty fields.

ApproachInput delay
controlled, state at the top140 ms
controlled, state per field16 ms
uncontrolled, read on submit12 ms

I would not consider it settled without evidence: Measure input delay on a mid-range device at the real field count for both approaches.

Control the fields that drive something else.

Curated: · Written: · Reviewed:

QA-36What makes a good custom hook, and what should not be one?(show answer)

Before changing anything for custom hooks, I would write down how I will know it worked.

A custom hook shares stateful logic rather than markup, and its value lies in a clear contract such as returning data, an error, and a loading flag. Logic with no hooks inside belongs in a plain function, which is easier to test and reuse.

Concretely, name the hook for what it provides, keep it to one concern, return a stable shape, and extract pure logic into ordinary functions.

The reason for that specificity is a failure I have seen: A single dashboard hook fetched 6 resources and held 9 pieces of state for 4 components, so a change for one caller broke the other three twice in a month.

What belongs where.

LogicShould be
formatting a priceplain function
subscribing to a connectioncustom hook
fetching 6 unrelated resourcesseveral hooks

I would not consider it settled without evidence: Test the hook in isolation and confirm each caller needs only the values it uses.

A hook shares behaviour, so keep its contract small.

Curated: · Written: · Reviewed:

QA-37What is a ref for, beyond holding a DOM node?(show answer)

I would start refs for values that do not render from what the user sees and measures, not from the framework's API.

A ref is a mutable box whose change does not trigger a render, which makes it right for DOM nodes, timer ids, previous values, and bookkeeping an effect needs to read without re-running. Anything the screen displays belongs in state.

Concretely, use refs for imperative handles and non-rendered bookkeeping, read and write them in effects and handlers rather than during render, and keep displayed values in state.

The reason for that specificity is a failure I have seen: A component stored its selected row in a ref, so clicking a row updated the ref but the highlight never moved until an unrelated update re-rendered the table.

Three values.

ValueRefState
DOM nodeyesno
interval idyesno
selected row shown in the UInoyes
3 values2 belong in a ref1 in state

I would not consider it settled without evidence: Change the value and check whether the screen must update, and if it must, keep it in state.

Refs remember, state renders.

Curated: · Written: · Reviewed:

QA-38When is a reducer better than several pieces of separate state?(show answer)

The first question I ask about reducers for state that moves together is which render or interaction it changes.

A reducer centralizes transitions when several values change together or when the next state depends on an event rather than one input. It also makes the legal transitions explicit and testable as a pure function.

Concretely, model the state as one object with a typed action union, keep the reducer pure, test transitions directly, and keep separate simple state for values that are genuinely independent.

The reason for that specificity is a failure I have seen: A checkout screen kept 7 booleans in separate state, and a race between two of them let the confirm button submit twice, creating 23 duplicate orders in one day.

Two designs for one screen.

DesignIllegal states reachableTestable
7 separate booleansmanythrough the UI only
reducer with 5 actionsnoneas a pure function

I would not consider it settled without evidence: Test the reducer's transitions directly and confirm no sequence of actions reaches an illegal state.

One reducer beats seven booleans that must agree.

Curated: · Written: · Reviewed:

QA-39What are the failure modes of fetching data inside a component?(show answer)

With the four states of data fetching, a passing local build is where I start checking rather than stop.

Fetching in a component has to handle loading, error, empty, cancellation, and out-of-order responses, and it re-runs whenever its inputs change. Most bugs come from the states nobody designed rather than from the request itself.

Concretely, model the four states explicitly, abort on input change and unmount, key any cache by the request inputs, and offer a retry rather than a silent empty screen.

The reason for that specificity is a failure I have seen: A profile page rendered an empty state for a failed request, and because the error was swallowed, 9 percent of sessions saw a blank screen with no way to retry.

What each state shows.

StateScreen
loadingskeleton
errormessage and retry
emptyexplanation
successthe data

I would not consider it settled without evidence: Force each state in a test, including a slow response followed by a fast one, and confirm the screen matches.

Design the four states, not just the happy path.

Curated: · Written: · Reviewed:

QA-40What do suspense and error boundaries actually cover?(show answer)

I would answer suspense boundaries and error boundaries by tracing one real interaction through the browser.

A suspense boundary shows a fallback while a child is waiting, and an error boundary catches errors thrown while rendering its subtree. Neither catches an error inside an event handler or an unhandled promise rejection, which still need their own handling.

Concretely, place boundaries around independent regions so one failure does not blank the page, keep fallbacks close in size to the real content to avoid layout shift, and handle handler and promise errors explicitly.

The reason for that specificity is a failure I have seen: A single error boundary at the root turned one failing widget into a blank page for every affected user, when a boundary per region would have lost only that widget.

What a boundary catches.

Error sourceCaught
rendering a childyes
effect bodyyes
event handlerno
unhandled rejectionno
4 error sources2 caught

I would not consider it settled without evidence: Throw inside one region and confirm the rest of the page still renders.

Put boundaries where you can afford to lose something.

Curated: · Written: · Reviewed:

QA-41How do you decide what to code split?(show answer)

My approach to deciding what to code split separates what the engine guarantees from what it usually does.

Splitting pays off for code most users do not need for the first screen, such as routes, modals, editors, and charts. Splitting something needed immediately adds a request and delays the first render.

Concretely, split by route first, then by heavy optional components, prefetch on intent such as hover or focus, and measure both the initial bundle and the interaction that now loads late.

The reason for that specificity is a failure I have seen: Lazy-loading the chart above the fold cut the initial bundle by 40 kilobytes but pushed the largest contentful paint from 1.9 to 3.4 seconds.

Three configurations.

SplitInitial bundleLargest contentful paint
none480 KB2.4 s
by route210 KB1.9 s
plus the hero chart170 KB3.4 s

I would not consider it settled without evidence: Compare initial bundle size and largest contentful paint before and after each split.

Split what users do not need yet.

Curated: · Written: · Reviewed:

QA-42You must render ten thousand rows. What do you do?(show answer)

For rendering very long lists, I would name the failure a user would notice before the technique.

Rendering every row costs DOM nodes, layout, and memory, so windowing renders only the visible rows plus a small buffer. The trade is that find-in-page, anchors, and assistive technology see only what is rendered, which the implementation has to account for.

Concretely, virtualize with a known row height where possible, measure dynamic heights, keep the scrollbar proportional, expose the total count to assistive technology, and offer search and filters instead of scrolling.

The reason for that specificity is a failure I have seen: A table rendered all 10,000 rows, which produced 140,000 DOM nodes, a 4.2 second first render, and scrolling below 20 frames per second.

Ten thousand rows.

ApproachDOM nodesFirst renderScrolling
every row140,0004,200 ms18 fps
windowed to 30 rows42090 ms60 fps

I would not consider it settled without evidence: Measure node count, first render, and scroll frame rate before and after windowing.

Render what is on screen, and account for what is not.

Curated: · Written: · Reviewed:

QA-43In a framework with server components, how do you decide what ships to the browser?(show answer)

I would test server and client component boundaries on a slow device and a real network, not only on a laptop.

Server components render on the server, can read data directly, and ship no JavaScript for themselves, while client components are required for state, effects, and browser APIs. The boundary decides how much JavaScript the user downloads.

Concretely, default to server components, move the interactive leaves to the client, pass only serializable props across the boundary, and keep data access and secrets on the server side.

The reason for that specificity is a failure I have seen: Marking a page-level layout as a client component pulled its entire subtree into the browser bundle, adding 210 kilobytes and 700 milliseconds of script evaluation on mobile.

Where the boundary sits.

BoundaryClient JavaScript for the route
at the layout310 KB
at the interactive leaves100 KB

I would not consider it settled without evidence: Inspect the client bundle for the route and confirm only the interactive leaves are in it.

Push the client boundary as far down the tree as it will go.

Curated: · Written: · Reviewed:

QA-44How would you choose between server rendering, static generation, and client rendering for a page?(show answer)

The part of choosing a rendering strategy that interviewers probe is the edge case, not the syntax.

The choice follows how fresh the data must be and whether it is user-specific. Static output is fastest and cacheable, server rendering suits personalized or frequently changing pages, and client rendering suits highly interactive views behind a login where first paint matters less.

Concretely, make shareable pages static and revalidate them on a schedule or on publish, server-render personalized pages, and hydrate only what needs interactivity.

The reason for that specificity is a failure I have seen: A marketing page was client-rendered behind a spinner, so crawlers and slow devices saw an empty shell and the largest contentful paint was 5.1 seconds.

Three pages, three strategies.

PageStrategyLargest contentful paint
marketingstatic with revalidation1.2 s
dashboardserver rendered2.0 s
editor behind loginclient rendered2.6 s

I would not consider it settled without evidence: Compare time to first byte, largest contentful paint, and the served HTML for each strategy.

Match the rendering strategy to freshness and personalization.

Curated: · Written: · Reviewed:

QA-45What causes a hydration mismatch, and how do you fix it?(show answer)

I would anchor hydration mismatches in a number from the profiler or the field, not in a preference.

Hydration fails when the HTML the server produced does not match what the client renders first, usually because the render read something that differs between them such as the current time, a random value, or the window. The framework then discards and re-renders, which can flash or break interactivity.

Concretely, keep the first client render deterministic, move browser-only values into an effect or a client-only component, pass server-computed values as props, and never branch on the window during render.

The reason for that specificity is a failure I have seen: A component rendered a relative timestamp during render, so the server produced 2 minutes ago and the client 3 minutes ago, and the mismatch blanked a section for users on slow connections.

Three values read during render.

ValueServerClientResult
relative timestamp2 min3 minmismatch
window widthunavailable390mismatch
prop from the serversamesameclean

I would not consider it settled without evidence: Render on the server and hydrate in a test and confirm no mismatch is reported.

The first client render must match the server's HTML.

Curated: · Written: · Reviewed:

QA-46How do you make an action feel instant without lying to the user?(show answer)

What separates a strong answer on optimistic updates is knowing why the naive version breaks.

An optimistic update applies the expected result immediately and reconciles when the server answers, which requires a rollback path and a visible failure state. It suits high-success reversible actions and is wrong for payments or anything irreversible.

Concretely, apply the change locally with a pending marker, send the request, roll back and explain on failure, deduplicate with an idempotency key, and treat the server response as the final truth.

The reason for that specificity is a failure I have seen: A like button applied its update optimistically with no rollback, so a failed request left the count showing 41 until the user reloaded the page.

Three actions.

ActionOptimisticReason
like a postyesreversible, high success
rename a fileyesreversible
submit a paymentnoirreversible

I would not consider it settled without evidence: Force the request to fail and confirm the screen rolls back with a message the user can act on.

Optimism needs a rollback to stay honest.

Curated: · Written: · Reviewed:

QA-47How do you test a component so the test survives a refactor?(show answer)

I would treat tests that survive a refactor as something every browser, input mode, and locale has to survive.

Tests that query by role, label, and text and assert user-visible behaviour survive refactors, while tests reaching into internals break when the implementation changes without the user noticing. The goal is confidence that a user can complete the task.

Concretely, render the component, query by accessible role or label, interact as a user with clicks and typing, assert on visible output, and mock at the network boundary rather than stubbing internals.

The reason for that specificity is a failure I have seen: A suite that asserted on internal state and class names broke 38 of 52 tests when a component was converted to hooks, although the user-facing behaviour had not changed.

Which queries survive.

QuerySurvives a refactor
by role, named Saveyes
by label, Emailyes
by CSS classno
inspecting internal stateno

I would not consider it settled without evidence: Refactor the internals without touching the markup and confirm every test still passes.

Test what the user does, not how the component stores it.

Curated: · Written: · Reviewed:

QA-48What makes a component accessible, and how do you check it?(show answer)

Before changing anything for accessible components by default, I would write down how I will know it worked.

Accessibility comes mostly from using the right element, labelling every control, keeping a visible focus order, and never conveying meaning by colour alone. The ARIA attributes fill gaps in native semantics rather than replacing them.

Concretely, use native buttons, links, and labels, manage focus when content appears or disappears, keep contrast at or above the required ratio, and test with the keyboard, a screen reader, and an automated checker.

The reason for that specificity is a failure I have seen: A styled container with a click handler served as the primary action, so keyboard users could not reach it and 4 percent of users could not finish signing up.

Three ways to make a control.

PatternKeyboard reachableAnnounced as
container with a click handlernonothing
button elementyesbutton with its name
anchor with a destinationyeslink

I would not consider it settled without evidence: Complete the main task with the keyboard alone and confirm each control is announced with its role and name.

The right element is most of accessibility.

Curated: · Written: · Reviewed:

QA-49How do you design the props of a reusable component?(show answer)

I would start designing a component API from what the user sees and measures, not from the framework's API.

A good component API is small, hard to misuse, and open where users need it, which usually means a few well-named props, composition through children for layout variety, and no growing pile of booleans that encode unrelated states.

Concretely, prefer composition over configuration, replace several booleans with one variant union, forward the remaining DOM props and the ref, and document intended use with examples.

The reason for that specificity is a failure I have seen: A button grew to 19 boolean props, two of which could be set together in a combination that produced unreadable text, and that combination reached production 4 times.

Two APIs for one button.

APIPropsInvalid combinations
19 booleans19many
variant union and children4none

I would not consider it settled without evidence: Try to express the wrong combination and confirm the types reject it.

Make the wrong usage impossible to express.

Curated: · Written: · Reviewed:

QA-50When do you reach for a state management library instead of the framework's own tools?(show answer)

The first question I ask about choosing a state management approach is which render or interaction it changes.

A library earns its place when many distant components read overlapping slices of shared state, when updates are frequent enough that context re-renders hurt, or when server-cache concerns such as caching, revalidation, and deduplication dominate. Otherwise local state and a small context are simpler.

Concretely, separate the server cache from client state, use a data library for the former and local state or a small store with selectors for the latter, and keep derived data out of the store.

The reason for that specificity is a failure I have seen: A team kept 80 percent of its server data in a global store with hand-written invalidation, which produced 14 stale-data bugs in a quarter before the fetching layer was replaced.

Where each kind of data belongs.

DataBelongs in
fetched list of ordersserver cache library
open modal and wizard steplocal state
session and themesmall global store

I would not consider it settled without evidence: List what the store holds and confirm each entry is client state rather than a cached server response.

Cache server data, own client state.

Curated: · Written: · Reviewed:

QA-51What happens between the HTML arriving and the first pixels appearing?(show answer)

With the critical rendering path, a passing local build is where I start checking rather than stop.

The browser parses HTML into a document, blocks on stylesheets and synchronous scripts, builds the render tree, lays it out, paints, and composites. Anything that blocks parsing or style resolution delays the first paint for every user.

Concretely, serve critical HTML first, keep blocking stylesheets small, defer or mark scripts as modules, inline only what the first screen needs, and preload the fonts and images the first paint uses.

The reason for that specificity is a failure I have seen: A synchronous analytics script in the document head delayed the first paint by 900 milliseconds on a mid-range phone, because parsing stopped until the script finished downloading.

Moving one script.

Script placementFirst paint
synchronous in the head2,100 ms
deferred before the body ends1,200 ms

I would not consider it settled without evidence: Record a trace of the first load on a throttled connection and confirm no render-blocking request sits before the first paint.

Nothing paints until the blocking work is done.

Curated: · Written: · Reviewed:

QA-52What is the difference between layout, paint, and composite, and why does it matter?(show answer)

I would answer reflow and repaint by tracing one real interaction through the browser.

Changing geometry forces layout and then paint and composite, changing only colours forces paint, and changing transform or opacity can be handled by the compositor alone. The later in that pipeline a change lands, the cheaper it is.

Concretely, animate transform and opacity rather than width, top, or margin, promote animated layers deliberately, and avoid reading layout properties in the middle of a write loop.

The reason for that specificity is a failure I have seen: A menu animated its height, which forced layout on every frame and produced 22 frames per second on a mid-range phone, while the same motion with a transform held 60.

The same animation, two properties.

Animated propertyPipeline stagesFrame rate
heightlayout, paint, composite22 fps
transformcomposite60 fps

I would not consider it settled without evidence: Record a performance profile during the animation and confirm no layout appears in the frame.

Animate what the compositor can do alone.

Curated: · Written: · Reviewed:

QA-53A style will not apply even though your selector looks right. How do you work out why?(show answer)

My approach to CSS specificity and cascade layers separates what the engine guarantees from what it usually does.

The winning declaration is decided by origin and layer, then specificity, then document order, with the important flag overriding all of it. A more specific selector elsewhere, not a broken rule, is the usual cause.

Concretely, inspect the computed styles to see which rule won, keep specificity low and flat, use cascade layers or a single utility system to make order predictable, and treat the important flag as a last resort.

The reason for that specificity is a failure I have seen: A component's styles were overridden by a global rule with an id selector, and the fix applied by the team was an important flag that then broke the dark theme on 6 pages.

Which rule wins.

SelectorSpecificityWins against a class
one class0-1-0tie, later wins
one id1-0-0yes
class with the important flag0-1-0yes, overrides

I would not consider it settled without evidence: Read the computed style panel and name the rule that won before changing any selector.

Find the winning rule before writing a stronger one.

Curated: · Written: · Reviewed:

QA-54Why does an element with a set width end up wider than expected?(show answer)

For the box model and sizing, I would name the failure a user would notice before the technique.

By default width applies to the content box, so padding and border add to the rendered size. Setting box-sizing to the border box makes width include padding and border, which is why most projects set it globally.

Concretely, set border-box sizing once for every element, prefer logical properties for padding and margin so right-to-left layouts work, and avoid fixed heights that clip text at larger font sizes.

The reason for that specificity is a failure I have seen: A 300 pixel card with 16 pixels of padding and a 1 pixel border rendered at 334 pixels, which broke a three-column grid on tablets at 1,024 pixels wide.

One card, two sizing models.

SizingDeclared widthRendered width
content box300 px334 px
border box300 px300 px

I would not consider it settled without evidence: Measure the rendered box in the inspector and confirm it matches the intended grid arithmetic.

Decide once what width means.

Curated: · Written: · Reviewed:

QA-55When do you reach for flexbox and when for grid?(show answer)

I would test flexbox and grid on a slow device and a real network, not only on a laptop.

Flexbox lays out along one axis and is best when content size should decide the distribution, while grid defines rows and columns in two dimensions and is best when the layout structure is known. Fighting one into the other's job is the usual source of fragile CSS.

Concretely, use flexbox for toolbars, button rows, and anything that should wrap by content, use grid for page and card layouts, and let the grid template define the structure rather than nesting several flex containers.

The reason for that specificity is a failure I have seen: A dashboard built from 5 nested flex containers needed 9 media queries to stay aligned, and a single grid with named areas replaced all of them.

The same dashboard.

ApproachContainersMedia queries
nested flexbox59
one grid with areas12

I would not consider it settled without evidence: Rebuild the layout in the other model and compare the number of rules and breakpoints needed.

One axis is flexbox, two is grid.

Curated: · Written: · Reviewed:

QA-56A dropdown appears behind another element even with a high z-index. Why?(show answer)

The part of stacking context and layering that interviewers probe is the edge case, not the syntax.

A z-index only orders siblings inside the same stacking context, and properties such as transform, filter, and opacity below one create a new context. An element inside a lower context can never rise above a sibling of its ancestor.

Concretely, keep overlays in a portal at the document root, avoid creating stacking contexts on ancestors of overlays, and use a small documented scale of z-index values rather than escalating numbers.

The reason for that specificity is a failure I have seen: A card with a transform created a stacking context, so its dropdown with a z-index of 9999 still rendered behind the next card, and the value was raised 4 times before the cause was found.

Why the high value loses.

Elementz-indexStacking contextResult
dropdown in a transformed card9,999the cardbehind
dropdown in a root portal10the rootin front

I would not consider it settled without evidence: Inspect the ancestors for properties that create a stacking context before changing any z-index.

A z-index only competes inside its own context.

Curated: · Written: · Reviewed:

QA-57How do you build a layout that works on any screen without sniffing the device?(show answer)

I would anchor responsive layout without device detection in a number from the profiler or the field, not in a preference.

Responsive layout follows the content and the container, not a list of known devices. Fluid sizing, wrapping, and container queries adapt to whatever space exists, while user-agent detection breaks on the next device.

Concretely, start from a single-column fluid layout, add breakpoints where the content stops looking right, size type and spacing with relative units, use container queries for reusable components, and test at arbitrary widths rather than device presets.

The reason for that specificity is a failure I have seen: A layout keyed to 3 known phone widths broke on a foldable at 673 pixels, leaving a 40 percent empty column that support reported for 3 weeks.

Continuous resize check.

WidthDevice-keyed layoutFluid layout
390 pxcorrectcorrect
673 pxbroken columncorrect
1,180 pxcorrectcorrect

I would not consider it settled without evidence: Resize the viewport continuously from 320 to 1,600 pixels and confirm no width produces a broken layout.

Design for the space, not for a device list.

Curated: · Written: · Reviewed:

QA-58How would you choose between utility classes, CSS modules, and runtime CSS-in-JS?(show answer)

What separates a strong answer on choosing a styling approach is knowing why the naive version breaks.

The trade is authoring convenience against runtime cost and predictability. Utility classes and CSS modules resolve at build time and ship plain CSS, while runtime CSS-in-JS computes styles during render, which adds JavaScript and can serialize styles on every update.

Concretely, prefer build-time styling for anything on the critical path, keep a documented token set so approaches stay consistent, and if a runtime library is already in use, measure its cost before adding more of it.

The reason for that specificity is a failure I have seen: A runtime styling library recomputed styles for 800 list rows on every keystroke, adding 90 milliseconds of scripting per input event.

Scripting cost per keystroke.

ApproachStyle workBundle addition
runtime CSS-in-JS90 ms14 KB
CSS modules0 ms0 KB
utility classes0 ms0 KB

I would not consider it settled without evidence: Profile one interaction and report how much scripting time the styling layer accounts for.

Style at build time on the critical path.

Curated: · Written: · Reviewed:

QA-59How do you load a web font without a flash of invisible or unstyled text?(show answer)

I would treat web fonts and text rendering as something every browser, input mode, and locale has to survive.

A font that blocks text rendering hides content until it loads, and one that swaps late shifts the layout. The display strategy decides which of those the user experiences, and matching the fallback metrics decides how much the swap moves.

Concretely, subset the font, serve modern formats, preload the one face the first screen needs, use a swap strategy with size-adjusted fallback metrics, and avoid loading several weights that the design does not use.

The reason for that specificity is a failure I have seen: Four font weights totalling 310 kilobytes blocked text for 1.6 seconds on a slow connection, and the later swap shifted the headline by 14 pixels.

Before and after.

SetupText visible atLayout shift
4 weights, blocking1,600 ms0.14
1 subset weight, swap with matched metrics300 ms0.01

I would not consider it settled without evidence: Load the page on a throttled connection and measure both the time text becomes visible and any layout shift the swap causes.

Show text early and make the swap invisible.

Curated: · Written: · Reviewed:

QA-60What does it take to serve images properly on a content page?(show answer)

Before changing anything for serving images well, I would write down how I will know it worked.

Images are usually the largest bytes on a page and the usual largest contentful paint element. Serving the right size for the viewport, a modern format, explicit dimensions, and lazy loading below the fold addresses most of the cost.

Concretely, provide a source set with widths and sizes, serve modern formats with a fallback, set width and height or an aspect ratio to reserve space, lazy-load below the fold, and never lazy-load the hero image.

The reason for that specificity is a failure I have seen: A hero image served at 2,400 pixels wide to every device cost 1.1 megabytes on mobile and made the largest contentful paint 4.8 seconds.

The same hero on mobile.

DeliveryBytesLargest contentful paint
one 2,400 px file1,100 KB4.8 s
source set and modern format180 KB1.9 s

I would not consider it settled without evidence: Compare transferred bytes and largest contentful paint on a mobile profile before and after the change.

Send the pixels the device can actually use.

Curated: · Written: · Reviewed:

QA-61Your largest contentful paint is 4 seconds. How do you find and fix the cause?(show answer)

I would start improving largest contentful paint from what the user sees and measures, not from the framework's API.

The metric measures when the largest element in the viewport renders, so the fix depends on whether the delay is in the server response, in resource loading, or in render-blocking work. Guessing without the breakdown usually optimizes the wrong stage.

Concretely, identify the element, split the time into time to first byte, resource load delay, load duration, and render delay, then attack the largest part with priority hints, caching, or a smaller resource.

The reason for that specificity is a failure I have seen: A team spent a sprint shrinking JavaScript while the largest contentful paint element was a hero image discovered late by the parser, and the metric improved by 0.2 seconds.

Where four seconds went.

StageBeforeAfter
time to first byte0.5 s0.5 s
resource load delay2.4 s0.3 s
load duration0.8 s0.6 s
render delay0.3 s0.3 s

I would not consider it settled without evidence: Report the four-part breakdown for the metric and show which part the change reduced.

Fix the stage that owns the time.

Curated: · Written: · Reviewed:

QA-62Users say the page feels slow to respond even though it loads fast. What do you measure?(show answer)

The first question I ask about interaction responsiveness is which render or interaction it changes.

Responsiveness is the delay between an input and the next paint that shows its result, so it is dominated by long tasks on the main thread rather than by load time. The worst interactions, not the average, are what users remember.

Concretely, measure the slowest interactions in the field, break long tasks into smaller chunks, yield to the browser between units of work, move heavy computation to a worker, and keep event handlers free of layout thrashing.

The reason for that specificity is a failure I have seen: A filter interaction ran a 420 millisecond synchronous computation in the click handler, so the checkbox did not visibly change for nearly half a second and users clicked it repeatedly.

One filter click.

ImplementationLongest taskInteraction delay
synchronous filter420 ms470 ms
chunked with yields40 ms90 ms
worker plus paint first12 ms40 ms

I would not consider it settled without evidence: Measure the interaction-to-paint delay at the 75th percentile in the field before and after the change.

Responsiveness is a main-thread problem.

Curated: · Written: · Reviewed:

QA-63Content jumps while the page loads. What causes that and how do you stop it?(show answer)

With layout stability, a passing local build is where I start checking rather than stop.

Layout shifts happen when content is inserted or resized after paint, most often by images without dimensions, late-loading fonts, injected banners, and ads. Reserving space in advance is what prevents them.

Concretely, set dimensions or an aspect ratio on media, reserve space for banners and embeds, avoid inserting content above existing content, and animate with transforms rather than properties that move neighbours.

The reason for that specificity is a failure I have seen: A cookie banner injected after paint pushed the page content down by 120 pixels, and 6 percent of sessions recorded a click on the wrong element within a second of the shift.

Reserving space.

ElementBeforeAfter
hero image without dimensions0.180.00
injected banner0.110.00
total shift score0.290.02, residual from a late embed

I would not consider it settled without evidence: Measure the cumulative layout shift in the field and confirm the largest shifting element is gone.

Reserve the space before the content arrives.

Curated: · Written: · Reviewed:

QA-64The bundle has grown to two megabytes. How do you bring it down?(show answer)

I would answer bundle size and tree shaking by tracing one real interaction through the browser.

Bundle growth is usually a few large dependencies rather than application code, and tree shaking removes only what the bundler can prove is unused. Measuring composition before cutting avoids weeks spent on the wrong modules.

Concretely, analyse the bundle to rank modules by size, replace or drop the largest dependencies, import named exports from module builds, split by route, and add a size budget to continuous integration.

The reason for that specificity is a failure I have seen: A date library with every locale added 320 kilobytes to the main bundle, which was 34 percent of it, for formatting that a built-in formatter performs.

Three modules and the total.

ModuleBeforeAfter
date library with locales320 KB0 KB
chart library240 KB90 KB, lazy
icon set110 KB6 KB, per icon
total main bundle940 KB310 KB

I would not consider it settled without evidence: Publish the bundle composition before and after and enforce a size budget that fails the build on regression.

Measure what is in the bundle before cutting.

Curated: · Written: · Reviewed:

QA-65How should a frontend build be cached so users get updates without refetching everything?(show answer)

My approach to caching and hashed assets separates what the engine guarantees from what it usually does.

Hashed filenames let static assets be cached permanently, because a change produces a new name. The HTML entry point must not be cached that way, or users keep loading an old document that points at old assets.

Concretely, hash every static asset and serve it with a long immutable cache, serve HTML with a no-cache or short revalidation policy, and keep chunk names stable so unrelated changes do not invalidate everything.

The reason for that specificity is a failure I have seen: The HTML document was served with a one-day cache, so users kept the old document and referenced a deleted script bundle, producing 4,800 script errors before the header was changed.

Two kinds of response.

ResponseCache policyOn deploy
hashed scriptimmutable, 1 yearnew name fetched
HTML documentno-cacherevalidated each visit

I would not consider it settled without evidence: Deploy a change and confirm returning users receive the new HTML and reuse unchanged assets from cache.

Cache the assets forever and never the entry point.

Curated: · Written: · Reviewed:

QA-66A page makes six sequential requests before it can render. How do you flatten that?(show answer)

For request waterfalls and resource hints, I would name the failure a user would notice before the technique.

A waterfall forms when each request is discovered only after the previous one finishes, so the fix is to let the browser learn about critical resources earlier. Preload, preconnect, and early data loading remove the discovery delay rather than making requests faster.

Concretely, preload the critical font, image, and script for the first screen, preconnect to required third-party origins, move data fetching to the server or start it in parallel, and remove chained client-side redirects.

The reason for that specificity is a failure I have seen: A font discovered inside a lazily imported stylesheet loaded 1.9 seconds after the first byte, because three requests had to complete before the browser knew it existed.

When the font starts.

SetupFont request startsFirst contentful paint
discovered through a chain1,900 ms2,600 ms
preloaded in the document120 ms1,300 ms

I would not consider it settled without evidence: Compare the network waterfall before and after and confirm the critical resources start in the first round trip.

Tell the browser early what it will need.

Curated: · Written: · Reviewed:

QA-67When is a service worker worth adding, and what can go wrong?(show answer)

I would test service workers and offline behaviour on a slow device and a real network, not only on a laptop.

A service worker is a programmable proxy that enables offline use and instant repeat loads, and it is also the easiest way to serve permanently stale code. Its lifecycle and update rules are the hard part, not the caching itself.

Concretely, cache the shell and static assets with a clear strategy per request type, version the cache and clean up old entries on activation, never cache the HTML entry indefinitely, and provide a way to force an update.

The reason for that specificity is a failure I have seen: A cache-first strategy on the HTML document served a build from 3 weeks earlier to returning users, and only clearing site data fixed it.

Strategy per request.

RequestStrategy
hashed assetcache first
HTML documentnetwork first
API datanetwork only, or short cache

I would not consider it settled without evidence: Deploy a change and confirm a returning user receives the new version on the next load rather than after clearing storage.

A service worker can cache a bug forever.

Curated: · Written: · Reviewed:

QA-68A long task blocks the main thread for half a second. What are your options?(show answer)

The part of breaking up long tasks that interviewers probe is the edge case, not the syntax.

Anything over roughly 50 milliseconds on the main thread delays input and animation, so the work must be split, deferred, or moved off the thread. Which option fits depends on whether the result is needed for the next paint.

Concretely, chunk the work and yield between chunks, defer non-urgent work to an idle callback, move pure computation to a worker, and render a first useful frame before finishing the rest.

The reason for that specificity is a failure I have seen: Parsing and sorting a 12 megabyte response on the main thread produced a 1.4 second task during which every click and keystroke was ignored.

One heavy parse.

ApproachLongest taskInput ignored for
all on the main thread1,400 ms1,400 ms
chunked with yields45 msnone noticeable
worker8 ms on the main threadnone

I would not consider it settled without evidence: Confirm no task exceeds 50 milliseconds in a profile of the interaction.

Give the main thread back between pieces of work.

Curated: · Written: · Reviewed:

QA-69A single-page application slows down after twenty minutes of use. How do you investigate?(show answer)

I would anchor memory leaks in long-lived pages in a number from the profiler or the field, not in a preference.

In a page that never reloads, retained listeners, timers, caches, and detached DOM nodes accumulate, and the symptom is gradual slowdown rather than a crash. Heap snapshots over time show what is retained and by whom.

Concretely, take heap snapshots at intervals and compare retained sizes, look for detached nodes and growing arrays, remove listeners and timers in cleanup, bound every cache, and re-test with a long scripted session.

The reason for that specificity is a failure I have seen: A chart component added a resize listener on every mount without removing it, and after 40 navigations the page held 40 listeners and 220 megabytes, with interactions 3 times slower.

Forty navigations.

BuildListenersHeapInteraction
leaking40220 MB3 times slower
with cleanup170 MBunchanged

I would not consider it settled without evidence: Run a scripted session of 50 navigations and confirm heap size and listener count return to a stable level.

In a page that never reloads, cleanup is the feature.

Curated: · Written: · Reviewed:

QA-70Your local audit scores well but users complain the site is slow. Why?(show answer)

What separates a strong answer on lab and field performance data is knowing why the naive version breaks.

A lab audit runs one synthetic profile on one device and network, while field data records what real users on real devices experience. The two disagree whenever the real audience is slower than the lab profile, which is usually.

Concretely, collect field metrics from real sessions, segment by device class, network, and country, set targets on the 75th percentile rather than the average, and use the lab only to reproduce and debug.

The reason for that specificity is a failure I have seen: A local audit reported a 1.4 second largest contentful paint while the field 75th percentile was 4.6 seconds, because 60 percent of users were on mid-range Android phones.

Lab against field.

SourceLargest contentful paint
local audit, desktop1.4 s
field, all users at the 75th percentile4.6 s
field, mid-range Android6.1 s

I would not consider it settled without evidence: Report the field 75th percentile by device class alongside the lab score.

Lab data debugs, field data decides.

Curated: · Written: · Reviewed:

QA-71How do you handle focus when opening a dialog and when navigating in a single-page application?(show answer)

I would treat focus management as something every browser, input mode, and locale has to survive.

Keyboard and screen-reader users follow focus, so it must move deliberately when content appears or the route changes. Focus that stays behind or escapes a modal leaves those users stranded in content they cannot see.

Concretely, move focus into the dialog on open, trap it while open, restore it to the trigger on close, and on route change move focus to the new heading or a skip target rather than leaving it on the old link.

The reason for that specificity is a failure I have seen: A modal did not trap focus, so pressing Tab moved into the page behind it, and a screen-reader user read 40 background links while the dialog was open.

Focus at each step.

EventFocus should be
dialog opensfirst control in the dialog
Tab at the last controlback to the first
dialog closesthe trigger that opened it
route changenew page heading

I would not consider it settled without evidence: Open and close the dialog with the keyboard alone and confirm focus enters, stays, and returns to the trigger.

Move focus where you moved the user's attention.

Curated: · Written: · Reviewed:

QA-72When should you add ARIA attributes, and when do they make things worse?(show answer)

Before changing anything for using ARIA correctly, I would write down how I will know it worked.

ARIA changes how assistive technology reports an element without changing behaviour, so a wrong role promises something the component does not do. Native elements already carry the right semantics, which is why adding ARIA is usually a sign the wrong element was chosen.

Concretely, use native elements first, add ARIA only for patterns with no native equivalent, keep names and states accurate, never put a role on an element that contradicts it, and test the result with a screen reader.

The reason for that specificity is a failure I have seen: A container was given a button role but no keyboard handler, so assistive technology announced a button that did nothing when activated, and the issue passed automated checks.

Three annotations.

MarkupAnnouncedWorks
container with a button role, no key handlerbuttonno
button elementbuttonyes
container with a link role and no destinationlinkno
3 annotations1 works2 mislead

I would not consider it settled without evidence: Operate every ARIA-annotated component with a screen reader and confirm the announced role matches what it does.

No ARIA beats wrong ARIA.

Curated: · Written: · Reviewed:

QA-73Automated accessibility checks pass. What do they miss?(show answer)

I would start testing with assistive technology from what the user sees and measures, not from the framework's API.

Automated tools catch contrast, missing names, and invalid attributes, which is roughly a third of real issues. They cannot judge whether the reading order makes sense, whether a change is announced, or whether a task can be completed by keyboard.

Concretely, keep the automated checker in continuous integration, then walk the main tasks with the keyboard and a screen reader, verify that dynamic updates are announced, and include one non-visual pass per release.

The reason for that specificity is a failure I have seen: A checkout passed every automated check while its error summary was never announced, so screen-reader users heard nothing after a failed submit and 30 percent abandoned.

What each pass catches.

CheckIssues found
automated scanner12
keyboard walkthrough7 more
screen-reader walkthrough5 more

I would not consider it settled without evidence: Complete the main task with a screen reader and confirm every state change is announced.

Automated checks are the floor, not the test.

Curated: · Written: · Reviewed:

QA-74What breaks when a product is translated and mirrored for right-to-left languages?(show answer)

The first question I ask about internationalization and right-to-left is which render or interaction it changes.

Translated text changes length, plural rules, and date and number formats, and right-to-left layouts mirror direction. Hard-coded widths, concatenated sentences, and physical left and right properties are what break.

Concretely, use the platform formatters for dates, numbers, and plurals, pass variables into whole translated sentences rather than concatenating fragments, use logical properties for spacing, and test with a long language and a right-to-left one.

The reason for that specificity is a failure I have seen: A button sized for the English label clipped its German translation at 34 characters, and the mirrored layout left icons on the wrong side for every Arabic user.

One label in three locales.

LocaleLabel lengthRenders
English12 charsfits
German34 charsclipped before the fix
Arabic14 charsmirrored layout

I would not consider it settled without evidence: Render the main screens in a long language and in a right-to-left language and confirm no clipping or mirrored-icon errors.

Test the longest language and the mirrored one.

Curated: · Written: · Reviewed:

QA-75How do you build an animation that stays smooth on a mid-range phone?(show answer)

With animation that holds its frame rate, a passing local build is where I start checking rather than stop.

A frame has roughly 16 milliseconds at 60 hertz, and only transform and opacity can be handled by the compositor without layout or paint. Anything that runs layout per frame, or that runs long JavaScript alongside, drops frames.

Concretely, animate transform and opacity, keep animation work off the main thread where possible, avoid animating many elements at once, respect the reduced-motion preference, and profile on a mid-range device rather than a laptop.

The reason for that specificity is a failure I have seen: A list animation transitioned the top property on 60 items at once, which forced layout each frame and produced 19 frames per second on a mid-range phone.

Sixty items animating.

PropertyLayout per frameFrame rate
topyes19 fps
transformno58 fps

I would not consider it settled without evidence: Record the frame rate on a mid-range device during the animation and confirm it holds close to the display refresh rate.

Animate on the compositor and profile on a real phone.

Curated: · Written: · Reviewed:

QA-76Where does cross-site scripting come from in a modern frontend, and how do you prevent it?(show answer)

I would answer cross-site scripting by tracing one real interaction through the browser.

Any path that turns data into markup or executable context can inject script, so the risk lives in raw HTML insertion, attribute and URL interpolation, and unsafe evaluation. Frameworks escape text by default, which is why the remaining holes are the deliberate escapes from that default.

Concretely, render data as text, sanitize with a vetted library when raw HTML is genuinely required, validate that URLs use an allowed scheme, never build markup by string concatenation, and add a content security policy as a second line.

The reason for that specificity is a failure I have seen: A comment field rendered raw HTML so a stored payload executed for every viewer, and 3,100 sessions loaded an attacker's script before the field was escaped.

Three injection paths.

PathSafe by defaultNeeds
text interpolationyesnothing
raw HTML insertionnosanitizer
href built from inputnoscheme allowlist

I would not consider it settled without evidence: Submit a script payload through every field and confirm it renders as text rather than executing.

Treat every value from a user or an API as text until proven otherwise.

Curated: · Written: · Reviewed:

QA-77What is cross-site request forgery, and what actually stops it?(show answer)

My approach to cross-site request forgery separates what the engine guarantees from what it usually does.

If a request carries credentials automatically, another site can cause the browser to send it. Same-site cookie attributes stop most of it, and a token the attacker cannot read covers the rest, while checking the request method or origin alone does not.

Concretely, set session cookies to a same-site policy, require a token for state-changing requests, verify the origin on the server, and never perform a state change on a request that only reads.

The reason for that specificity is a failure I have seen: A password-change endpoint accepted a form post with cookies and no token, so a malicious page changed the password of any logged-in user who visited it, and 40 accounts were taken over.

What each control blocks.

ControlBlocks cross-site form postBlocks a malicious script on your own page
same-site cookieyesno
synchronizer tokenyesno
content security policynoyes

I would not consider it settled without evidence: Submit the state-changing request from a different origin and confirm the server rejects it.

Automatic credentials are the whole problem.

Curated: · Written: · Reviewed:

QA-78How should a single-page application handle access and refresh tokens?(show answer)

For handling auth tokens in a browser app, I would name the failure a user would notice before the technique.

A token readable by JavaScript is exposed to any script on the page, so the safest arrangement keeps the session in a cookie the server marks as inaccessible to scripts and refreshes it server-side. Where a token must reach the client, it should be short-lived and never persisted.

Concretely, prefer server-set cookies with same-site and secure attributes, keep access tokens short-lived, refresh through an endpoint rather than in client storage, handle a failed refresh by returning the user to sign-in, and never put a token in a URL.

The reason for that specificity is a failure I have seen: Refresh tokens were kept in local storage, and one compromised dependency allowed silent session renewal for an attacker for 11 days.

Three arrangements.

StorageReadable by scriptsSurvives a script compromise
local storageyesno
memory onlyyes, while runningpartly
cookie inaccessible to scriptsnoyes

I would not consider it settled without evidence: Confirm from the console that no token is reachable by page scripts and that a revoked session stops working immediately.

Keep the long-lived credential out of the page.

Curated: · Written: · Reviewed:

QA-79A request works in a tool but fails in the browser with a CORS error. What is happening?(show answer)

I would test cross-origin resource sharing on a slow device and a real network, not only on a laptop.

The browser, not the server, blocks a cross-origin response the server has not permitted, so a CORS error is the server declining to opt in rather than a client bug. Requests that are not simple also require a preflight to succeed first.

Concretely, have the server return the allowed origin, methods, and headers, handle the preflight request, set credentials on both sides when cookies are needed, and never use a wildcard origin together with credentials.

The reason for that specificity is a failure I have seen: A team proxied every API call through their own server to silence CORS errors, which added 180 milliseconds per request and hid the fact that one endpoint was never meant to be public.

What the browser needs.

RequestPreflightServer must return
simple getnoallowed origin
get with a custom headeryesallowed origin, headers
post with cookiesyesexact origin, credentials allowed

I would not consider it settled without evidence: Inspect the preflight and the response headers and confirm the server allows the exact origin, method, and headers used.

A CORS error is a server policy message.

Curated: · Written: · Reviewed:

QA-80What does a content security policy buy you, and how do you roll one out without breaking the site?(show answer)

The part of a content security policy that interviewers probe is the edge case, not the syntax.

A policy limits where scripts, styles, images, and connections may come from, which turns many injection bugs into blocked requests. Its value depends on not weakening it with unsafe inline allowances, which is also what makes adoption hard on an existing site.

Concretely, start in report-only mode, collect violations from real traffic, replace inline scripts with nonces or hashes, tighten the directives, then enforce, and keep reporting on afterwards.

The reason for that specificity is a failure I have seen: A policy enforced without a reporting period blocked the payment provider's script for 90 minutes, and checkout failed for every user until it was rolled back.

Rollout stages.

StageDurationViolations
report only1 week240
after fixes3 days4
enforcedongoing0

I would not consider it settled without evidence: Run the policy in report-only mode for a full traffic cycle and confirm the violation report is empty before enforcing.

Report first, enforce second.

Curated: · Written: · Reviewed:

QA-81Marketing wants to add three tag-manager scripts. What do you say?(show answer)

I would anchor third-party scripts and dependency risk in a number from the profiler or the field, not in a preference.

A third-party script runs with the same privileges as your own code, so it can read the page, slow it down, and change behaviour without a deploy on your side. The cost is both a performance and a security decision.

Concretely, inventory what each script does and who owns it, load non-critical ones after interaction or in a worker, pin versions and use integrity hashes where possible, restrict them with a content security policy, and measure their cost before and after.

The reason for that specificity is a failure I have seen: Three marketing scripts added 340 kilobytes and 1.2 seconds of main-thread work, and one of them injected a banner that shifted the layout by 0.22.

Three scripts measured.

ScriptBytesMain-thread timeLayout shift
tag manager120 KB400 ms0.00
chat widget180 KB600 ms0.02
banner tool40 KB200 ms0.22

I would not consider it settled without evidence: Measure main-thread time, transferred bytes, and layout shift with each third-party script disabled and enabled.

Every third-party script is code you did not review.

Curated: · Written: · Reviewed:

QA-82How do you find out about frontend errors that users hit but never report?(show answer)

What separates a strong answer on error monitoring and source maps is knowing why the naive version breaks.

Uncaught errors and rejected promises can be reported from the browser with enough context to reproduce, but a minified stack is unreadable without source maps. Reporting without grouping and release tagging produces noise nobody reads.

Concretely, capture uncaught errors and unhandled rejections, upload source maps privately at build time, tag every event with the release, group by fingerprint, and alert on a rate rather than on each event.

The reason for that specificity is a failure I have seen: An error affecting 4 percent of sessions went unnoticed for 3 weeks because the reports were unreadable minified frames and nobody had a release tag to compare against.

Before and after source maps.

ReportTop frameActionable
minifieda.b.c at line 1no
with source mapsCartTotal.tsx line 42yes

I would not consider it settled without evidence: Trigger a known error in production and confirm the report shows the original source and the release that introduced it.

An unreadable stack is an unread report.

Curated: · Written: · Reviewed:

QA-83A product manager asks you to track a new flow. How do you instrument it well?(show answer)

I would treat instrumenting product analytics as something every browser, input mode, and locale has to survive.

Useful analytics needs an agreed event name, properties, and firing condition per step, decided before the code is written. Events added ad hoc produce numbers nobody can define later.

Concretely, write a short tracking plan naming each event, its properties, and exactly when it fires, implement it in one place rather than across components, test events in a staging environment, and monitor daily volumes for sudden drops.

The reason for that specificity is a failure I have seen: A checkout redesign renamed the purchase event, so conversion reports showed zero purchases for 4 days until someone compared the dashboard with the payment provider.

Two rows of a tracking plan.

EventFires whenProperties
checkout_startedthe cart page is submittedcart_value, item_count
purchase_completedthe payment confirmsorder_id, revenue, currency

I would not consider it settled without evidence: Fire every event in a staging environment and confirm each arrives once with the agreed properties.

Name and define events before implementing them.

Curated: · Written: · Reviewed:

QA-84How would you ship a risky frontend change safely?(show answer)

Before changing anything for feature flags and safe rollout, I would write down how I will know it worked.

A flag separates deploying code from enabling behaviour, which allows a small exposure, a measured comparison, and an instant switch back without a deploy. Its cost is extra code paths that must be removed once the decision is made.

Concretely, put the change behind a flag, enable it for a small percentage, compare error rate and the key interaction metric against the control, expand in steps, and delete the flag and the dead branch after the rollout.

The reason for that specificity is a failure I have seen: A checkout rewrite shipped to every user at once, raised the error rate from 0.4 to 6 percent, and needed a 40-minute rollback deploy while sales were lost.

A staged rollout.

StageUsersError rateDecision
control95%0.4%baseline
stage 15%0.5%expand
stage 250%0.4%complete

I would not consider it settled without evidence: Compare error rate and the primary metric between flag groups before each expansion.

Separate deploying from enabling.

Curated: · Written: · Reviewed:

QA-85How do you decide which browsers to support and what to compile to?(show answer)

I would start browser support and build targets from what the user sees and measures, not from the framework's API.

Build targets should come from the audience's actual browser mix, because compiling for older engines adds bytes and slows modern ones. A target list nobody has reviewed usually costs every user for a few percent of traffic.

Concretely, read the real browser distribution from analytics, set an explicit target list, ship a modern build to modern browsers, load polyfills only where needed, and re-check the distribution each quarter.

The reason for that specificity is a failure I have seen: A build targeted a browser version with 0.1 percent of traffic, which added 62 kilobytes of transpiled output and polyfills to every modern user's bundle.

The audience against the target.

Browser groupShare of sessionsBundle
modern evergreen97.4%210 KB
legacy target0.1%272 KB

I would not consider it settled without evidence: Compare bundle size for the modern and the legacy target and report the share of traffic each serves.

Target the browsers your users actually run.

Curated: · Written: · Reviewed:

QA-86You maintain a shared component library used by four applications. How do you ship a breaking change?(show answer)

The first question I ask about versioning a design system is which render or interaction it changes.

A shared library's consumers upgrade on their own schedule, so a breaking change needs a version boundary, a migration path, and a deprecation period. Changing behaviour in a patch release breaks builds the consumers did not ask to change.

Concretely, follow semantic versioning strictly, add the new API alongside the old one, deprecate with a console warning and a dated removal, publish a migration note with examples, and provide a codemod for mechanical changes.

The reason for that specificity is a failure I have seen: A prop rename shipped in a patch version broke the build of 3 of 4 consuming applications on their next install, costing a day of work across those teams.

How the change ships.

ReleaseContainsConsumer action
minornew prop, old one deprecatednone required
minor plus 2 monthswarning in consolemigrate
majorold prop removedupgrade deliberately

I would not consider it settled without evidence: Install the new version in each consumer without code changes and confirm nothing breaks before the deprecation period ends.

Consumers upgrade on their timetable, not yours.

Curated: · Written: · Reviewed:

QA-87How do you decide what to test manually across browsers and devices?(show answer)

With cross-browser and device testing, a passing local build is where I start checking rather than stop.

Test where the audience and the risk are, which usually means the top browsers by traffic plus the oldest supported version, and one low-end device. Testing every combination is impossible, so the choice should follow the data.

Concretely, rank browsers and devices by session share, keep one low-end Android and one iOS device in the loop, automate the checks that catch rendering regressions, and test any feature that touches layout, input, or media on real devices.

The reason for that specificity is a failure I have seen: A date picker worked everywhere the team tested and failed on iOS Safari, which was 38 percent of the traffic, so a week of bookings were entered by hand.

Coverage against traffic.

BrowserSession shareIn the matrix
Chrome desktop41%yes
Safari on iOS38%yes, after the incident
Firefox6%yes
older Edge0.3%smoke only

I would not consider it settled without evidence: List session share per browser and device class and confirm the test matrix covers the top of that list.

Test where your users are, not where your laptop is.

Curated: · Written: · Reviewed:

QA-88How would you split unit, integration, and end-to-end tests for a frontend?(show answer)

I would answer balancing test types by tracing one real interaction through the browser.

Unit tests are fast and precise but prove little about a screen, end-to-end tests prove the flow works but are slow and flaky, and component integration tests sit in the middle. The right mix maximizes confidence per minute of pipeline time.

Concretely, cover pure logic with unit tests, cover screens with component tests that query as a user, keep a handful of end-to-end tests for the flows that make money, and remove any test that fails for reasons unrelated to the change.

The reason for that specificity is a failure I have seen: A suite of 240 end-to-end tests took 52 minutes and failed on unrelated changes about 30 percent of the time, so the team started ignoring red builds.

Before and after rebalancing.

LayerBeforeAfterRuntime
unit12030040 s
component302603 min
end to end240226 min

I would not consider it settled without evidence: Measure suite duration and the share of failures that were not caused by the change under test.

Confidence per minute is the thing to optimize.

Curated: · Written: · Reviewed:

QA-89How do you catch unintended visual changes without a wall of false positives?(show answer)

My approach to visual regression testing separates what the engine guarantees from what it usually does.

Screenshot comparison finds changes that assertions miss, and it also flags every intentional change and every rendering difference. It works only with stable rendering conditions and a review step for accepted changes.

Concretely, snapshot components rather than whole pages, pin fonts, animations, and dates, run in one container image, set a small pixel tolerance, and require a human to accept each intended change.

The reason for that specificity is a failure I have seen: Full-page screenshots compared across two machines produced 60 diffs a day from font rendering alone, and the team disabled the check within a month.

Two setups.

SetupDiffs per dayReal regressions caught
full pages, two machines601
components, pinned rendering21

I would not consider it settled without evidence: Run the suite twice on unchanged code and confirm zero diffs before trusting any failure.

A visual check is only useful if unchanged code produces no diff.

Curated: · Written: · Reviewed:

QA-90What should run in continuous integration for a frontend project, and in what order?(show answer)

For continuous integration for a frontend, I would name the failure a user would notice before the technique.

The pipeline should fail as early and as cheaply as possible, so fast static checks come before expensive browser tests. A pipeline that takes longer than a coffee break stops being run before merge.

Concretely, run type checks and lint first, then unit and component tests, then a production build with a bundle-size budget, then end-to-end tests against a preview deployment, and cache dependencies and build output between runs.

The reason for that specificity is a failure I have seen: A pipeline that ran end-to-end tests first took 18 minutes to report a type error, so developers pushed small fixes repeatedly and the queue grew to 40 minutes.

Time to first failure.

OrderType error reported atTotal green run
browser tests first18 min24 min
static checks first50 s11 min

I would not consider it settled without evidence: Measure time to first failure for a type error, a failing unit test, and a broken flow.

Fail fast on the cheap checks.

Curated: · Written: · Reviewed:

QA-91Should a page work without JavaScript in 2026?(show answer)

I would test progressive enhancement on a slow device and a real network, not only on a laptop.

The useful version of this question is whether the core task survives partial failure, because scripts fail on slow networks, blocked third parties, and errors even when JavaScript is enabled. Server-rendered markup and real form submissions keep the core task available.

Concretely, render the critical content on the server, make forms submit without a script and enhance them afterwards, keep navigation as real links, and measure how often the enhancement fails to load.

The reason for that specificity is a failure I have seen: A signup form rendered only after a script bundle loaded, and 1.3 percent of sessions saw an empty page because that bundle failed on flaky mobile networks.

With the bundle blocked.

PageContent visibleTask completable
script-rendered formnoneno
server-rendered formallyes

I would not consider it settled without evidence: Load the main flows with scripting disabled and confirm the core task still completes.

The core task should survive a failed bundle.

Curated: · Written: · Reviewed:

QA-92How do you handle a design that cannot be built as specified?(show answer)

The part of working with designers that interviewers probe is the edge case, not the syntax.

Most such conflicts come from cases the design did not cover, such as long text, empty states, errors, or small screens, rather than from an impossible visual. Bringing the specific case and an option is more productive than refusing the design.

Concretely, build the design at real content extremes early, show the designer the failing case in the browser, agree a rule for it, and capture the resolution in the component's documentation so it is not re-litigated.

The reason for that specificity is a failure I have seen: A card design assumed 3-line titles, real data reached 11 lines, and the layout was patched independently in 4 places before anyone asked the designer for a rule.

Cases a design should cover.

CaseSpecifiedAgreed rule
11-line titlenoclamp to 3 lines with a tooltip
empty listnoexplanatory empty state
320 px widthnostack the actions

I would not consider it settled without evidence: Render the component with the longest real content and an empty state and review both with the designer.

Bring the failing case, not an objection.

Curated: · Written: · Reviewed:

QA-93How do you estimate a frontend feature?(show answer)

I would anchor estimating frontend work in a number from the profiler or the field, not in a preference.

Frontend estimates are usually wrong because the visible layout is a fraction of the work, while states, edge cases, accessibility, and cross-browser behaviour are most of it. Breaking the feature into those parts produces an estimate that survives review.

Concretely, list the states, the responsive and right-to-left behaviour, accessibility, tests, and the integration with real data, estimate each, add the review and fix cycle, and give a range with the largest unknown named.

The reason for that specificity is a failure I have seen: A screen estimated at 2 days from the mockup took 9, because the mockup covered 1 of 6 states and the shared table component needed a change.

Where nine days went.

PartEstimatedActual
happy-path layout2 d2 d
loading, empty, error states02 d
accessibility and keyboard01 d
shared component change02 d
tests and review02 d

I would not consider it settled without evidence: Compare the estimate against actual time afterwards and record which category was underestimated.

Estimate the states, not the mockup.

Curated: · Written: · Reviewed:

QA-94What do you look for when reviewing a frontend pull request that touches shared code?(show answer)

What separates a strong answer on reviewing a risky pull request is knowing why the naive version breaks.

The risk in shared frontend code is usually not the logic but the blast radius, which includes every consumer, every state, and rendering behaviour that tests do not cover. A review should ask what else renders this and what happens on the paths nobody demonstrated.

Concretely, check the consumers of anything changed, look for missing loading and error states, watch for reference-unstable props and new blocking requests, check accessibility of new controls, and ask for a screenshot or preview of the real states.

The reason for that specificity is a failure I have seen: A shared table change looked correct in the author's screen and re-rendered 4 other pages on every keystroke, which was found by users rather than by review.

A review checklist for shared code.

QuestionWhy
who else imports thisblast radius
which states are demonstratedmissing error and empty paths
any new blocking requestfirst paint cost
keyboard and focus behaviouraccessibility regressions

I would not consider it settled without evidence: List the consumers of the changed module and confirm the author exercised at least the highest-traffic ones.

Ask what else renders this.

Curated: · Written: · Reviewed:

QA-95A bug appears only in production. How do you approach it?(show answer)

I would treat debugging a production-only bug as something every browser, input mode, and locale has to survive.

Production differs from development in build optimizations, environment configuration, real data, network conditions, caching, and third-party scripts. Narrowing which of those differs is faster than reading code.

Concretely, reproduce against a production build locally, compare configuration and feature flags, check cached assets and service-worker state, use source-mapped error reports and session context, and add temporary logging behind a flag when needed.

The reason for that specificity is a failure I have seen: A crash that appeared only in production came from a minification-unsafe assumption about a function name, and 3 days were spent reading application logic before anyone built locally in production mode.

Differences to rule out.

DifferenceCheck
minified buildrun a production build locally
environment configcompare values, not assumptions
stale cachetest a fresh profile and a returning one
third-party scriptdisable and retest

I would not consider it settled without evidence: Reproduce the failure against a production build before changing any application code.

Find which difference matters before reading the code.

Curated: · Written: · Reviewed:

QA-96How do you stop a fast page from slowly getting slower?(show answer)

Before changing anything for owning a performance budget, I would write down how I will know it worked.

Performance decays through many small additions that each look acceptable, so it holds only when a budget is enforced automatically and owned by someone. A dashboard nobody is accountable for does not prevent regression.

Concretely, set budgets for bundle size and the field metrics, fail the build when a budget is exceeded, review the field numbers weekly with a named owner, and require a trade to be named when a budget increase is requested.

The reason for that specificity is a failure I have seen: A page that launched with a 210 kilobyte bundle grew to 940 kilobytes over 8 months in increments of 20 to 60 kilobytes, and none of the pull requests was individually questioned.

Budgets that fail the build.

BudgetLimitCurrent
main bundle250 KB210 KB
largest contentful paint, field 75th percentile2.5 s1.9 s
interaction delay, field 75th percentile200 ms90 ms

I would not consider it settled without evidence: Show the build failing on a deliberate budget breach and the weekly field review with its owner named.

A budget only works if the build enforces it.

Curated: · Written: · Reviewed:

QA-97Tell me about a time you made something measurably faster.(show answer)

I would start a performance win worth telling from what the user sees and measures, not from the framework's API.

Interviewers want the measurement, the diagnosis, the change, and the verified result, in that order. A story without a before and after number sounds like a preference rather than engineering.

Concretely, name the metric and its starting value, describe how you found the cause rather than guessing, state the change, give the after value from the field, and mention what you chose not to do.

The reason for that specificity is a failure I have seen: A candidate described removing a library and making the page feel much faster, could not give either number, and the panel could not distinguish the result from a placebo.

The shape of the answer.

PartExample
metricfield largest contentful paint, 4.6 s
diagnosishero image discovered late, 2.4 s load delay
changepreload plus a responsive source set
result1.9 s at the 75th percentile, 2 weeks later

I would not consider it settled without evidence: Rehearse the story with the before and after figures and the source of each.

A performance story lives on its two numbers.

Curated: · Written: · Reviewed:

QA-98Tell me about a time you disagreed with a designer or a product manager.(show answer)

The first question I ask about disagreeing about a product or design decision is which render or interaction it changes.

The answer should show that you understood their goal, brought evidence rather than opinion, and accepted the decision once it was made by whoever owns it. Being right is less interesting to an interviewer than being useful.

Concretely, state the disagreement and the goal behind it, describe the evidence you gathered such as a measurement or a prototype, say who decided, and say what you did afterwards.

The reason for that specificity is a failure I have seen: A candidate described refusing to build an animation they considered wasteful, escalating twice, and the panel read it as someone who would relitigate settled decisions.

A disagreement that worked.

PartExample
their goala livelier landing page
my concern400 KB animation library
evidenceprototype showed 1.1 s slower on mobile
outcomelighter CSS animation, goal met

I would not consider it settled without evidence: Check that your story names the evidence you brought and the decision you then supported.

Disagree with evidence, then commit.

Curated: · Written: · Reviewed:

QA-99Design the frontend for an activity feed with live updates, filters, and infinite scroll.(show answer)

With designing a feed or dashboard screen, a passing local build is where I start checking rather than stop.

A feed is a data-freshness and rendering-cost problem more than a layout problem. The design has to decide how pages are keyed, how live updates merge without moving the content under the reader, and what happens on a slow network.

Concretely, page with a cursor rather than an offset, cache by filter, windowing for long lists, buffer live updates behind a show-new-items control, preserve scroll position across navigation, and define loading, empty, and error states per region.

The reason for that specificity is a failure I have seen: A feed used offset paging and prepended live items directly, so new posts shifted the list while users read and 12 percent of clicks landed on the wrong item.

Four decisions.

ConcernDecision
pagingcursor based
live updatesbuffered behind a control
long listswindowed at 30 rows
filterscache keyed by filter

I would not consider it settled without evidence: Insert live items while the user is scrolled into the list and confirm nothing moves under the pointer.

Never move content under a reader.

Curated: · Written: · Reviewed:

QA-100In a live coding round, build a modal dialog. What must it do?(show answer)

I would answer building an accessible dialog live by tracing one real interaction through the browser.

A dialog is the standard test of whether a candidate knows the platform, because it needs focus management, an escape route, background inertness, and correct semantics. Interviewers watch for those rather than for the visual.

Concretely, render it in a portal, move focus to the first control on open, trap focus while open, close on Escape and on backdrop click, restore focus to the trigger, mark it as a dialog with an accessible name, and make the background inert.

The reason for that specificity is a failure I have seen: A dialog built in an interview looked correct and left the background scrollable and focusable, so the candidate could not explain what a screen-reader user would hear.

The checklist.

RequirementImplementation
focus on openfirst control in the dialog
focus trapcycle within the dialog
Escapecloses and restores focus
semanticsdialog role with an accessible name
backgroundinert and not scrollable
5 requirementsall graded live

I would not consider it settled without evidence: Operate the dialog with the keyboard alone and confirm focus enters, stays, and returns, and that the background is inert.

A dialog is graded on focus, not on the overlay.

Curated: · Written: · Reviewed: