laggy type-to-filter — keeps input responsive, drops no keystrokes
One line: debounce = once AFTER quiet · throttle = steady DURING stream · rAF = align to paint · transition = change render priority. They're orthogonal.
useDeferredValue vs debounce — NOT substitutes: deferred = render priority of data you already have (no dropped keystrokes); debounce = how often a side effect / API call fires (drops values). useDeferredValuedoesn't suppress effects → fetch still fires per committed value. Autocomplete hitting a server = BOTH (debounce the request + defer the render).
Render elsewhere — off the main thread
Web Worker — JS on a separate thread, no DOM, talk via postMessage. For CPU-heavy work (parse/sort/filter 50k, image proc). Comlink = call it like a function.
OffscreenCanvas — render canvas (map/chart) from a worker.
Can't move it? Chunk + await scheduler.yield() (old: setTimeout 0) between chunks → not one long task. Key INP fix.
Diagnose first (Lesson 04)
React Profiler → which components re-render & why → reach for memo.
Perf panel → long tasks (>50ms) → render-rarely vs render-elsewhere.
1,000s of nodes → virtualize · handler storm → debounce/throttle/transition · one fat fn → worker/yield.
Lead one-liners (memorize)
Frame “One thread does JS, layout, paint AND my click handlers — rendering perf is three moves on it: render less, render rarely, render elsewhere.”
Re-render “Re-renders fire on state, parent, or context — not props alone. memo/useMemo/useCallback stop the wasted ones but cost too, so I measure first; the React Compiler increasingly does it for me.”
Virtualize “A 1,000-result list shouldn't be 1,000 DOM nodes — I window it. Cost is find-in-page, a11y, SEO, so for indexed content I'd use content-visibility instead.”
Rarely “Debounce fires once after typing stops; throttle at a steady rate during a stream; rAF aligns to the frame. For a laggy filter I reach for useDeferredValue — responsive, no dropped keys.”
Elsewhere “Genuinely heavy CPU work — no memo saves you — goes to a Web Worker so the main thread stays free; if I can't move it, I chunk it and yield.”
1. Re-render triggers = state / parent / context. Props alone don't — they change because parent re-rendered, and parent re-render re-renders ALL children.
2.Memoization is NOT free (comparison + memory + stale-dep bugs). Measure first, memoize the hot path. React Compiler (19) auto-memoizes — stop hand-wrapping everything.
3.Never index-as-key on reordering/filtering lists → state sticks to wrong row + breaks reconciliation. Stable ID from data.
4. Big list → virtualize. But it breaks Ctrl-F / a11y / SEO → don't virtualize indexed SSR content; use content-visibility.
5.Debounce ≠ throttle ≠ rAF ≠ transition. Laggy filter → useDeferredValue (no dropped keystrokes), not a 1s debounce. But useDeferredValuedoes NOT replace debounce for the network — it doesn't suppress effects, so the fetch still fires per value. Server autocomplete = debounce the request AND defer the render.
6. 200ms compute → Web Worker / yield. useMemo just caches — it still runs on the main thread and still freezes.
7. Worker cost = startup + structured-clone serialization across the boundary + no DOM. Don't ping-pong big objects; use transferables / SharedArrayBuffer.