learn
JavaScript

Performance patterns

Debounce, throttle, memoization — and doing less, less often.

Fast pages are rarely clever algorithms; they are fewer handlers, fewer renders, fewer requests. Interviewers ask this because debounce/throttle/ memoize are three memorizable shapes that fix entire classes of jank.

Real-life situation

A search box fetches on every keystroke — ten requests for "javascript", nine of them wasted, the slowest winning. Debouncing to 300ms of silence fires once with the final text. The scroll feed beside it uses throttle instead: position checks at most every 100ms while scrolling continues.

Seen in: Lodash ships battle-tested debounce and throttle with cancel, flush and leading/trailing options (Lodash, 2012, debounce docs).

Visual

Silence, then firebrowser

Keystrokes

r

Timer

fetch("r") in 300ms

Fetches

empty

Console

—

Type "r" at 0ms. The handler records the query and starts a 300 ms timer. If nothing else happens, this timer fires the fetch.

Step 1 of 5
scenario.js
onType("r");    // t = 0ms
onType("re");   // t = 200ms
onType("red");  // t = 400ms
// quiet for 300ms…
fetch("red");   // ONE request

Try it

Debounce from scratch
javascriptTab moves to the next control
function debounce(fn, wait) {
let timer = null;
return function () {
  const args = arguments;
  const self = this;
  clearTimeout(timer);
  timer = setTimeout(function () {
    fn.apply(self, args);
  }, wait);
};
}
let searches = 0;
const search = debounce(function (q) {
searches = searches + 1;
}, 20);
search("t");
search("te");
search("tea");
await sleep(60);
console.log(searches);
Press Run to see the output.
Expected output
1
Runner helpers

sleep(ms) waits; fakeFetch(url) returns deterministic sample data. Console code runs in a worker with a 5-second limit.

Try dropping the clearTimeout line and count the calls. Then add a cancel method on the returned function that clears the timer.

When each applies

Debounce bursty, restartable work (search, resize, autosave). Throttle continuous feedback (scroll, mousemove). Memoize pure, expensive, repeated calls — and cap the cache before it becomes the leak. Otherwise: delegate, bail early, render less.

Cached call — guess first, then reveal.

predict.js
function memoize(fn) {
const cache = {};
return function (key) {
  if (!(key in cache)) {
    cache[key] = fn(key);
  }
  return cache[key];
};
}
let calls = 0;
const price = memoize(function (id) {
calls = calls + 1;
return id * 10;
});
console.log(price(2));
console.log(price(2));
console.log(calls);

Interview questions

What is debouncing, and when do you use it?basic

Debounce waits until calls STOP for a set time, then runs once — the search box that fetches only after you pause typing. Implementation is one closed-over timer: clear on every call, set anew. Use it for bursty, restartable work (search, resize, autosave). The tell that you need it: the same expensive function firing per keystroke or per pixel.

Interviewers also ask

  • What goes wrong without it on a search box?
What is throttling, and how is it different from debouncing?basic

Throttle guarantees at most one run per interval — the scroll handler updating at most every 100ms while scrolling continues. Debounce waits for silence; throttle meters a steady drip during noise. Scroll, mousemove and resize want throttle (continuous feedback); search and save want debounce (final intent). Saying "drip versus silence" plus one example of each is full marks.

Interviewers also ask

  • Which one for infinite scroll position checks?
How do you implement debounce from scratch?intermediate

Return a wrapper closing over a timer id: on each call, clear the pending timer and start a fresh setTimeout for the wait period. Forward this and arguments (fn.apply(this, args)) so the wrapper is transparent, and expose a cancel that clears the timer for teardowns. Five lines of mechanism, one line of forwarding — write it cold in interviews by remembering "clear, set, forward".

Interviewers also ask

  • How do you preserve this and arguments?
What is memoization and when does it backfire?intermediate

Caching function results by arguments: same input, no recompute — ideal for pure, expensive, repeated calls (formatters, derived data). It backfires on huge or unbounded argument spaces (the cache becomes the leak — cap it), on impure functions (stale truth), and on cheap functions (lookup overhead exceeds the saving). useMemo is the same idea wearing component clothes, with the same caveats.

Interviewers also ask

  • When does the cache become a leak?
What is the once pattern?basic

A wrapper ensuring the function runs exactly one time, returning the cached result afterwards — for init routines, analytics setup, singleton creation. Implementation: a called flag plus cached value in a closure. It is memoization with a single slot, and the answer to every "but what if two components initialize it?" race in interviews.

Interviewers also ask

  • How is once different from memoize?
How do you keep a list rendering fast with thousands of rows?intermediate

Render only the visible window (virtualization) — a thousand-row DOM is slow to build, style and scroll no matter how clean the code. Below that threshold: keyed updates (no index keys on reorderable lists), memoized rows, and batched inserts (DocumentFragment, single innerHTML build). Name virtualization first — it is the expected headline — then the small-list hygiene as the follow-through.

Interviewers also ask

  • Why are index keys harmful on reorder?
What is the cheapest performance win for event handlers?intermediate

Doing less, less often: delegate instead of binding per element, throttle scroll/resize handlers, debounce inputs, and bail out early when nothing changed. Most handler slowness is repetition (a hundred listeners, a handler per pixel), not algorithmics — remove the repetition before reaching for workers or memoization. Profile first: the flame chart usually points at one over-eager listener.

Interviewers also ask

  • How do you find the slow handler?
What is the difference between lazy loading and code splitting?basic

Code splitting divides the bundle into loadable chunks; lazy loading DEFERS some chunks until needed (route change, first interaction, viewport entry). Together: small initial download, features on demand. The interview angle is measurement — split by route, lazy-load below the fold and heavy widgets, and verify with the bundle analyzer, not vibes.

Interviewers also ask

  • How do you verify a split helped?
How do requestAnimationFrame and layout thrashing fit together?intermediate

Browsers paint on their own rhythm (~60fps); requestAnimationFrame schedules your visual updates just before a paint, keeping motion smooth. Layout thrashing is the anti-pattern: alternating reads and writes of geometry (offsetHeight, then style changes) in a loop, forcing a reflow per iteration. Fix: batch reads, then batch writes, ideally inside one animation frame — measure-then-mutate, once.

Interviewers also ask

  • What forces a synchronous reflow?

Key takeaways

  • Debounce waits for silence; throttle meters a steady drip.
  • Memoize pure repeated calls; cap the cache.
  • Cheapest wins are structural: delegate, bail early, render less.

References

Last verified: 2026-10-12.

On this page