JS and React Fundamentals (Uber R2/R3) ​
Uber frequently drops fundamentals questions into R3 between coding problems, and sometimes saves a pure-theory chunk for R2. L5A means you're expected to answer these without hesitation and go one layer deeper than L4 would. This file is the complete cheat sheet.
Section 1: Event Loop ​
The pieces ​
JavaScript runs on a single thread. The event loop coordinates four main things:
- Call stack — the execution stack of function frames.
- Task queue (macrotask queue) —
setTimeout,setInterval, I/O callbacks, UI events. - Microtask queue —
Promise.then,queueMicrotask,MutationObserver. - Render pipeline —
requestAnimationFramecallbacks, style recalc, layout, paint.
The loop's actual order (simplified) ​
- Run the oldest macrotask to completion.
- Drain the microtask queue completely (new microtasks added during draining also run).
- If a render is needed (roughly every 16.7ms), run
requestAnimationFramecallbacks, then style/layout/paint. - Back to step 1.
Why Promise.then fires before setTimeout(0) ​
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("sync");
// Output: sync → promise → timeoutThe current synchronous script is itself a macrotask. When it finishes, the microtask queue drains before the next macrotask. Promise.then schedules a microtask; setTimeout schedules a macrotask. Microtasks always win within the same turn.
queueMicrotask ​
Manually schedule a microtask. Useful when you want something to happen before the next render but after the current stack clears.
queueMicrotask(() => {
// runs after current synchronous code, before any setTimeout
});requestAnimationFrame ​
Scheduled before the next paint. Callbacks get a high-resolution timestamp argument. Use this for visual animations so the browser can batch DOM reads and writes.
Tricky interview trace ​
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => {
console.log("C");
setTimeout(() => console.log("D"), 0);
});
queueMicrotask(() => console.log("E"));
console.log("F");
// Output: A → F → C → E → B → DWalkthrough:
AandFare synchronous.- After sync, drain microtasks:
Cruns, which also queuesDas a macrotask and enqueues no new microtask forEbecauseEwas already queued beforeC's then. Order of microtasks is FIFO: first-added first. The.thencallback was added beforequeueMicrotaskin the call order, soCbeforeE. - Then macrotasks:
B(queued first),D(queued later).
When this matters in interviews ​
Uber loves event-loop traces because they separate people who have memorized hooks from people who understand the runtime. If you get a trace question, write out the three queues on a whiteboard and literally tick through each step.
Section 2: Promises and Async ​
Promise states ​
pending → fulfilled (with a value) or rejected (with a reason). Once settled, never changes.
Chaining ​
fetchUser(id)
.then((user) => fetchOrders(user.id)) // returning a promise chains it
.then((orders) => render(orders))
.catch((err) => showError(err))
.finally(() => setLoading(false));- Returning a value from a
.thenwraps it in a fulfilled promise. - Returning a promise from a
.thencauses the chain to wait for it. - Throwing in a
.thenrejects the next link. .finallygets no value and cannot change the chain's value.
async/await under the hood ​
async/await is syntactic sugar for generators + promises. The function is paused at each await and resumed when the awaited promise settles. The return value of an async function is always a promise.
async function x() {
const user = await fetchUser();
return user.name;
}
// roughly equivalent to:
function x() {
return Promise.resolve()
.then(() => fetchUser())
.then((user) => user.name);
}Promise.all / any / race / allSettled ​
| Method | Resolves when | Rejects when | Use case |
|---|---|---|---|
all | All resolve | Any rejects | Parallel work that all must succeed |
any | Any resolves | All reject | "First one to succeed wins" (fastest replica) |
race | First settles (either way) | First settles if rejection | Timeout patterns |
allSettled | All settle | Never | You need every outcome, success or failure |
Implementations (from scratch) ​
export function promiseAll<T>(list: Array<Promise<T> | T>): Promise<T[]> {
return new Promise((resolve, reject) => {
if (list.length === 0) return resolve([]);
const results: T[] = new Array(list.length);
let remaining = list.length;
list.forEach((p, i) => {
Promise.resolve(p).then(
(v) => {
results[i] = v;
if (--remaining === 0) resolve(results);
},
reject,
);
});
});
}
export function promiseAny<T>(list: Array<Promise<T> | T>): Promise<T> {
return new Promise((resolve, reject) => {
if (list.length === 0) {
return reject(new AggregateError([], "All promises were rejected"));
}
const errors: any[] = new Array(list.length);
let remaining = list.length;
list.forEach((p, i) => {
Promise.resolve(p).then(resolve, (e) => {
errors[i] = e;
if (--remaining === 0) reject(new AggregateError(errors, "All promises were rejected"));
});
});
});
}
export function promiseRace<T>(list: Array<Promise<T> | T>): Promise<T> {
return new Promise((resolve, reject) => {
for (const p of list) Promise.resolve(p).then(resolve, reject);
});
}
type Settled<T> =
| { status: "fulfilled"; value: T }
| { status: "rejected"; reason: any };
export function promiseAllSettled<T>(list: Array<Promise<T> | T>): Promise<Settled<T>[]> {
return promiseAll(
list.map((p) =>
Promise.resolve(p).then(
(value): Settled<T> => ({ status: "fulfilled", value }),
(reason): Settled<T> => ({ status: "rejected", reason }),
),
),
);
}Pitfalls ​
- Forgetting to
await—const user = fetchUser();gives you a Promise, not a user. TypeScript catches it; JS doesn't. - Unhandled rejections — A
.thenwith no.catchand no finalawaitleaks unhandled rejection warnings. Always terminate chains with.catchor await inside atry/catch. - Sequential vs parallel —
for (const id of ids) await fetchOne(id)is sequential. UsePromise.all(ids.map(fetchOne))for parallel. .theninside.then— pyramid of doom. Just return the inner promise from the outer.then.
When this matters in interviews ​
Uber will ask you to trace what a chain resolves to, to implement Promise.all from scratch, and to rewrite a callback soup into async/await. Practice each until it's muscle memory.
Section 3: Closures ​
Lexical scoping ​
A function "remembers" the variables in scope where it was defined, not where it was called. That persistent reference is a closure.
function outer() {
const secret = 42;
return function inner() {
return secret; // inner closes over `secret`
};
}
const f = outer();
f(); // 42Practical uses ​
Private state
function counter() {
let n = 0;
return {
inc: () => ++n,
get: () => n,
};
}once — run a function at most once
export function once<T extends (...args: any[]) => any>(fn: T): T {
let called = false;
let result: ReturnType<T>;
return ((...args: Parameters<T>) => {
if (called) return result;
called = true;
result = fn(...args);
return result;
}) as T;
}curry — break an n-ary function into unary calls
export function curry<T extends (...args: any[]) => any>(fn: T) {
return function curried(...args: any[]): any {
if (args.length >= fn.length) return fn(...args);
return (...rest: any[]) => curried(...args, ...rest);
};
}
const add = (a: number, b: number, c: number) => a + b + c;
const c = curry(add);
c(1)(2)(3); // 6
c(1, 2)(3); // 6Memoize with closure
export function memoize<A extends any[], R>(fn: (...args: A) => R): (...args: A) => R {
const cache = new Map<string, R>();
return (...args: A): R => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args);
cache.set(key, result);
return result;
};
}Factory functions — returning preconfigured behavior. debounce and throttle (below) are exactly this.
Common interview trap: the var in loop ​
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}var is function-scoped; all closures share the same i. Fix: use let (block-scoped), or wrap in an IIFE.
When this matters in interviews ​
Closures show up in every JS round. Uber's favorite is "implement memoize" or "what does this loop print and why." Know both cold.
Section 4: this Binding ​
The four rules (in precedence order) ​
newbinding —new Foo()—thisis the newly created object.- Explicit binding —
fn.call(obj),fn.apply(obj, args),fn.bind(obj). - Implicit binding —
obj.fn()—thisisobj. - Default binding — standalone
fn()—thisisundefinedin strict mode,windowotherwise.
Arrow functions ​
Arrow functions do not get their own this. They inherit from the enclosing lexical scope. call/apply/bind cannot change it.
const obj = {
x: 10,
regular() { return this.x; }, // 10 when called as obj.regular()
arrow: () => this, // `this` is whatever `this` was when the object literal was evaluated (global, undefined, etc.)
};call / apply / bind ​
call(thisArg, a, b)— invoke withthisbound, args as list.apply(thisArg, [a, b])— invoke withthisbound, args as array.bind(thisArg, a)— return a new function withthisand optional partial args bound.
bind polyfill ​
declare global {
interface Function {
myBind(this: Function, thisArg: any, ...bound: any[]): any;
}
}
Function.prototype.myBind = function (thisArg: any, ...bound: any[]) {
const fn = this as Function;
function bound_(this: any, ...called: any[]) {
// If called with `new`, ignore `thisArg` - use the newly created object
const isNew = this instanceof bound_;
return fn.apply(isNew ? this : thisArg, [...bound, ...called]);
}
// Preserve prototype chain so `instanceof` works for bound constructors
if (fn.prototype) {
bound_.prototype = Object.create(fn.prototype);
}
return bound_;
};Call with:
function greet(this: { name: string }, greeting: string) {
return `${greeting}, ${this.name}`;
}
const hi = greet.myBind({ name: "Aayush" }, "Hello");
hi(); // "Hello, Aayush"Common pitfalls in React callbacks ​
- Class component
onClick={this.handleClick}losesthisunless bound in the constructor or written as an arrow class field. - Passing
array.forEach(this.doThing)in a class method — same problem. - Modern function components with hooks avoid this entirely because there is no
this.
When this matters in interviews ​
Uber's R3 loves "what does this refer to here" puzzles. The bind polyfill is a standard ask - don't forget the new handling and the prototype chain line, because interviewers do check for both.
Section 5: Prototypes and Inheritance ​
The prototype chain ​
Every object has an internal [[Prototype]] (accessible as __proto__) pointing to another object. Property lookup walks up the chain.
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;
rabbit.eats; // true (inherited)
rabbit.jumps; // true (own)Object.create vs class ​
class is syntactic sugar over prototype chains plus constructor functions. class Foo {} creates a constructor function whose prototype is an object the class methods live on. Instances have __proto__ === Foo.prototype.
class Dog {
bark() { return "woof"; }
}
const d = new Dog();
Object.getPrototypeOf(d) === Dog.prototype; // trueImplement new from scratch ​
function myNew<T>(Constructor: new (...args: any[]) => T, ...args: any[]): T {
// 1. Create a new object whose prototype is Constructor.prototype
const obj = Object.create(Constructor.prototype);
// 2. Invoke the constructor with `this` bound to the new object
const result = (Constructor as any).apply(obj, args);
// 3. If the constructor returned an object, return that; otherwise return obj
return (result !== null && typeof result === "object") ? result : obj;
}
class Point {
constructor(public x: number, public y: number) {}
distance(): number { return Math.hypot(this.x, this.y); }
}
const p = myNew(Point, 3, 4);
p.distance(); // 5
p instanceof Point; // trueWhen this matters in interviews ​
instanceof checks walk the prototype chain. If you implement new you must set the prototype. If you bind a constructor, you must preserve the chain (see bind polyfill above). Prototype questions have been less common recently but still surface in L5A+ rounds.
Section 6: Debounce and Throttle ​
Definitions ​
- Debounce — delay execution until the caller has been idle for N ms. Use for "fire only at the end" (search input, resize settling).
- Throttle — execute at most once per N ms. Use for "fire at a steady rate" (scroll handlers, mouse-move).
When each fits ​
- Search autocomplete → debounce 150-300ms.
- Window resize settled → debounce.
- Scroll position tracker → throttle.
- Analytics pings during scroll → throttle.
Implementations ​
type Fn = (...args: any[]) => any;
export function debounce<T extends Fn>(
fn: T,
wait: number,
opts: { leading?: boolean; trailing?: boolean } = { trailing: true },
) {
let timer: ReturnType<typeof setTimeout> | null = null;
let lastArgs: Parameters<T> | null = null;
let didLeading = false;
const debounced = (...args: Parameters<T>) => {
lastArgs = args;
if (opts.leading && !timer && !didLeading) {
fn(...args);
didLeading = true;
lastArgs = null;
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
if (opts.trailing && lastArgs) fn(...lastArgs);
timer = null;
lastArgs = null;
didLeading = false;
}, wait);
};
debounced.cancel = () => {
if (timer) clearTimeout(timer);
timer = null;
lastArgs = null;
didLeading = false;
};
debounced.flush = () => {
if (timer && lastArgs) {
clearTimeout(timer);
fn(...lastArgs);
timer = null;
lastArgs = null;
}
};
return debounced;
}
export function throttle<T extends Fn>(
fn: T,
wait: number,
opts: { leading?: boolean; trailing?: boolean } = { leading: true, trailing: true },
) {
let last = 0;
let timer: ReturnType<typeof setTimeout> | null = null;
let lastArgs: Parameters<T> | null = null;
const throttled = (...args: Parameters<T>) => {
const now = Date.now();
if (!last && opts.leading === false) last = now;
const remaining = wait - (now - last);
lastArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timer) {
clearTimeout(timer);
timer = null;
}
last = now;
fn(...args);
lastArgs = null;
} else if (!timer && opts.trailing !== false) {
timer = setTimeout(() => {
last = opts.leading === false ? 0 : Date.now();
timer = null;
if (lastArgs) {
fn(...lastArgs);
lastArgs = null;
}
}, remaining);
}
};
throttled.cancel = () => {
if (timer) clearTimeout(timer);
timer = null;
last = 0;
lastArgs = null;
};
return throttled;
}React hooks ​
import { useEffect, useMemo, useRef, useState } from "react";
export function useDebouncedValue<T>(value: T, wait: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), wait);
return () => clearTimeout(t);
}, [value, wait]);
return debounced;
}
export function useThrottledCallback<T extends Fn>(fn: T, wait: number) {
const fnRef = useRef(fn);
fnRef.current = fn; // keep latest without re-creating the throttle
return useMemo(
() => throttle(((...args: any[]) => fnRef.current(...args)) as T, wait),
[wait],
);
}When this matters in interviews ​
Debounce/throttle polyfills are asked in 80% of Uber machine coding rounds. Write them from memory, with cancel, and explain leading vs trailing edge clearly.
Section 7: React Hooks Deep Dive ​
useState ​
Lazy initial state: If the initial value is expensive, pass a function. It runs only on first render.
const [items, setItems] = useState(() => expensiveCompute());Functional updates: When the next state depends on the previous, pass a function. Avoids stale closures.
setCount((c) => c + 1);Batching: React 18 batches state updates across timeouts, promises, and native events. Multiple setState calls in a row produce one re-render.
useEffect ​
Dependency array:
undefined→ runs after every render.[]→ runs once after mount; cleanup on unmount.[a, b]→ runs whenaorbchanges (byObject.is).
Cleanup runs before the next effect. If you set an interval and the deps change, cleanup runs first, then the new effect.
Race conditions in async effects:
useEffect(() => {
let alive = true;
fetchUser(id).then((u) => {
if (alive) setUser(u);
});
return () => { alive = false; };
}, [id]);Without the alive flag, a slow response for an old id can overwrite the latest user.
useMemo vs useCallback vs React.memo ​
| Hook | Memoizes | Use when |
|---|---|---|
useMemo | A value | The computation is expensive OR the value is a dep of another hook |
useCallback | A function reference | The function is a prop to a memoized child OR a dep of another hook |
React.memo | A component | Re-renders are expensive AND props usually don't change |
When each is NOT worth it:
useMemo(() => a + b, [a, b])— the compute is trivial; the memo overhead exceeds the saving.useCallbackon a function used only by non-memoized children — useless.React.memoon a cheap component with frequently changing props — actively slower because of the extra shallow compare.
useRef ​
Mutable container whose .current survives re-renders without triggering one. Use for:
- DOM references (
ref.current.focus()). - Instance-like mutable values (timers, previous values).
- Stable callbacks when you don't want
useCallback.
Custom hooks ​
A function whose name starts with use and calls other hooks. Rules:
- Only call hooks at the top level. No conditionals, loops, nested functions.
- Only call hooks from React functions or other custom hooks.
Composition example:
function useDebouncedSearch(query: string, wait = 200) {
const debounced = useDebouncedValue(query, wait);
const { data, loading } = useFetch(`/api/search?q=${encodeURIComponent(debounced)}`);
return { results: data, loading };
}useLayoutEffect vs useEffect ​
useEffectfires asynchronously after paint. User sees the update, then your effect runs.useLayoutEffectfires synchronously after DOM mutation, before paint. User never sees the intermediate state.
Use useLayoutEffect when you need to measure DOM and adjust before paint (tooltip positioning). Otherwise use useEffect; it doesn't block paint.
useReducer ​
Better than useState when:
- Multiple state fields that change together.
- Next state depends on the previous in non-trivial ways.
- You want logic testable outside the component (the reducer is a pure function).
type State = { count: number; history: number[] };
type Action = { type: "inc" } | { type: "dec" } | { type: "reset" };
function reducer(s: State, a: Action): State {
switch (a.type) {
case "inc": return { count: s.count + 1, history: [...s.history, s.count + 1] };
case "dec": return { count: s.count - 1, history: [...s.history, s.count - 1] };
case "reset": return { count: 0, history: [] };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, history: [] });When this matters in interviews ​
Expect "what's the difference between useMemo and useCallback" and "when is React.memo worth it." L5A candidates are expected to say "it's not always worth it" instead of blanket "use them always."
Section 8: React Rendering Model ​
Reconciliation and diffing ​
On each render, React creates a new virtual tree and diffs against the previous. For elements of the same type at the same position, React reuses the DOM node and updates props. For different types, React unmounts the old and mounts the new.
Keys ​
Keys tell React which item is which when reordering arrays. Bad key choice (array index) leads to input state leaking across items when the list reorders. Use stable, unique ids.
// Bad
items.map((item, i) => <Row key={i} item={item} />)
// Good
items.map((item) => <Row key={item.id} item={item} />)Controlled vs uncontrolled ​
- Controlled — React state is the source of truth;
valueis set from state,onChangeupdates state. - Uncontrolled — DOM is the source of truth; read via
ref.current.value.
Controlled is default. Uncontrolled is fine for simple forms, large text inputs where every keystroke re-rendering is wasteful, or when integrating with non-React code.
Context API re-renders ​
Any component consuming a Context re-renders when the provider's value changes, regardless of which field of value they use. The common bug:
<Provider value={{ user, setUser }}> // new object every renderFixes:
- Memoize the value:
useMemo(() => ({ user, setUser }), [user]). - Split into multiple contexts (one for rarely-changing config, one for hot state).
- Use a selector-based store (Zustand, Redux) for anything large.
Virtual DOM vs Real DOM ​
The virtual DOM is a JS object tree that mirrors the real DOM. React diffs virtual trees in memory, then applies only the changes to the real DOM. Real DOM operations are expensive (layout, paint); batching through the VDOM reduces them.
Fiber architecture ​
React 16+ rewrite. Fiber is a unit of work representing a component. Reconciliation is split into incremental work slices that can be paused and resumed. Enables Suspense, concurrent features, time-slicing (yield to the browser between long trees).
Just-enough-for-interview summary:
- Old React (stack reconciler) walked the tree recursively; couldn't interrupt.
- Fiber represents each component as a node in a linked list; work is cooperative; React can pause when the main thread has higher-priority work (animation, user input).
Strict Mode double-invoke ​
In development, Strict Mode intentionally runs effects, state updaters, and render twice to surface unintentional side effects. Production ignores Strict Mode. If your code breaks under Strict Mode, it's buggy - don't turn it off.
Suspense ​
Component for declarative loading states. Inside a <Suspense fallback={...}>, any component that throws a promise causes React to render the fallback until the promise resolves. Used by React.lazy, and by data-fetching libraries like Relay.
<Suspense fallback={<Spinner />}>
<LazyProfile userId={id} />
</Suspense>Error Boundaries ​
Class components (no hook equivalent yet) that catch errors in their child tree. static getDerivedStateFromError and componentDidCatch.
class ErrorBoundary extends React.Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(err: Error) { log(err); }
render() { return this.state.hasError ? this.props.fallback : this.props.children; }
}When this matters in interviews ​
Be crisp on keys (this is the single most common "why is this bug happening" React question), Context re-renders (senior-level signal), and Fiber at the level of "incremental work and interruptibility." Uber R3 has been known to ask "why does my Context consumer re-render when I didn't change the value my component uses."
Section 9: Performance Optimization ​
Memoization: when NOT worth it ​
- Cheap compute (simple arithmetic, short string ops).
- Values that change every render anyway.
- Components with frequently changing props.
- Small trees where re-render cost is under 1ms.
The rule: measure before optimizing. Profile with the React DevTools Profiler; pick the top 3 longest renders.
Virtualization ​
Render only the rows in the viewport plus a small overscan buffer. Libraries: react-window (simpler), @tanstack/react-virtual (more flexible).
import { FixedSizeList } from "react-window";
<FixedSizeList
height={500}
itemCount={10000}
itemSize={32}
width="100%"
>
{({ index, style }) => <div style={style}>Row {index}</div>}
</FixedSizeList>Code splitting ​
React.lazy + Suspense splits at route or feature boundaries.
const Admin = React.lazy(() => import("./Admin"));
<Suspense fallback={<Spinner />}>
<Admin />
</Suspense>Webpack/Vite chunk the import; the chunk loads only when <Admin /> renders.
Bundle size reduction ​
- Tree shaking (named imports from ES modules, no side-effect imports).
- Dynamic imports for rarely used code.
- Swap heavy libraries (moment → date-fns, lodash → lodash-es named imports).
- Use
@rollup/plugin-visualizerorrollup-plugin-analyzerto see what's big.
Image optimization ​
- Lazy loading:
<img loading="lazy" />or IntersectionObserver. - Responsive:
<img srcset="... 1x, ... 2x" sizes="...">to serve correct size. - Modern formats: WebP for wide support, AVIF for cutting edge (fallback via
<picture>).
Core Web Vitals ​
| Metric | Target | What frontend can do |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | Preload hero image, optimize critical CSS, server-side render |
| INP (Interaction to Next Paint) | < 200ms | Avoid long tasks, code-split, yield to main thread (scheduler.yield) |
| CLS (Cumulative Layout Shift) | < 0.1 | Reserve space for images/ads, avoid inserting content above existing content |
When this matters in interviews ​
System design will ask about bundle budgets and Core Web Vitals. Fundamentals rounds ask "when should you NOT use useMemo." Always answer with a cost/benefit frame.
Section 10: Browser and DOM ​
Event delegation ​
Attach one listener to a container; inspect event.target inside. Instead of N listeners, you have 1. Handles dynamically added children for free.
document.getElementById("list")!.addEventListener("click", (e) => {
const li = (e.target as HTMLElement).closest("li");
if (!li) return;
handleClick(li.dataset.id!);
});Bubbling vs capturing ​
Events travel in two phases:
- Capturing — from
windowdown through ancestors to the target. - Target — the element itself.
- Bubbling — from the target back up through ancestors.
addEventListener(type, handler) defaults to bubbling. Pass { capture: true } for the capturing phase. Use stopPropagation() to stop the current phase from continuing. Use stopImmediatePropagation() to also prevent other listeners on the same element.
IntersectionObserver ​
Declarative "is this element visible" API. Perfect for infinite scroll, lazy loading images, tracking ad impressions.
const observer = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) loadMore();
}
}, { rootMargin: "200px" });
observer.observe(sentinelEl);rootMargin: "200px" triggers 200px before the element enters the viewport, so you prefetch in advance.
ResizeObserver ​
Fires when an element's size changes. Replaces hacks like listening to window.resize plus checking element size manually.
const ro = new ResizeObserver((entries) => {
for (const e of entries) {
console.log(e.contentRect.width);
}
});
ro.observe(box);MutationObserver ​
Fires when the DOM tree changes (children added/removed, attributes changed). Use for syncing external libraries with React-rendered DOM, or detecting third-party injections.
Layout thrashing ​
When JS forces the browser to recompute layout repeatedly in the same frame. Caused by read-write-read-write patterns:
// Bad: each read forces layout after the previous write
for (const el of elements) {
el.style.width = el.offsetWidth + 10 + "px";
}
// Good: batch reads, then batch writes
const widths = elements.map((el) => el.offsetWidth);
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + "px";
});The cause: any layout-dependent read (offsetWidth, getBoundingClientRect, getComputedStyle, clientHeight) invalidated after a write forces synchronous layout.
requestAnimationFrame vs setTimeout ​
setTimeout(fn, 16)— "run after 16ms." Drifts, runs even if tab is hidden.requestAnimationFrame(fn)— "run before next paint." Throttled to screen refresh rate. Paused when tab is hidden. Correct for visual animation.
When this matters in interviews ​
Event delegation is asked directly. IntersectionObserver for infinite scroll is expected in system design. Layout thrash ("how do you diagnose jank") is a senior-signal question - mention DevTools Performance panel, long tasks, forced reflow markers.
Closing Reminders ​
- Event loop trace questions are the trickiest. Walk through queues literally.
- Implementing polyfills is 60% of R3. Practice bind, debounce, throttle, promise combinators until automatic.
- Know the cost side of every React optimization. "useMemo is not always free" is a senior answer.
- When in doubt, say what you'd measure, then what you'd change. Interviewers love profiling-first thinking.