Lesson 20 of 30
Debounce and throttle solve the same underlying performance problem two different ways, and there's a third tool for the specific case where the work is visual — knowing which one fits which event is the actual skill.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A search-as-you-type feature should fire an API request only after the user pauses typing, not on every keystroke. Which technique fits this requirement?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
You already know how to implement debounce and throttle. What matters here is a different question: for a real high-frequency event — scroll, resize, mousemove, input — which one do you actually reach for, and when should you reach for neither and use requestAnimationFrame instead?
The underlying problem is the same across all three: events like scroll and mousemove can fire dozens of times within a single frame. A handler that does real work — reading layout, computing a value, updating state — on every single tick is doing dozens of times more work than the user can possibly perceive, and that work competes with everything else on the main thread, including the browser's own ability to paint the next frame smoothly.
The decision comes down to what you actually need from the stream of events:
scroll event — capping it to, say, once every 100ms still looks smooth to a human eye while doing a fraction of the work.requestAnimationFrame specifically when the work is visual and needs to sync with the browser's actual paint cycle. Reading scroll position and updating a visual style is a better fit for rAF than for a millisecond-based throttle, because rAF callbacks run right before the browser paints, naturally capping at the display's real refresh rate and never doing visual work for a frame that's never going to be shown.// Bad: runs on every single scroll tick, potentially dozens of times per frame
window.addEventListener("scroll", () => {
layer.style.transform = `translateY(${window.scrollY * 0.5}px)`;
});
// Better, but still timer-based: capped to a fixed interval unrelated to paint timing
window.addEventListener("scroll", throttle(() => {
layer.style.transform = `translateY(${window.scrollY * 0.5}px)`;
}, 16));
// Best fit: synced to the browser's actual paint cycle, never wasted on an
// unshown frame, and naturally capped at the display's real refresh rate
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
layer.style.transform = `translateY(${window.scrollY * 0.5}px)`;
ticking = false;
});
ticking = true;
}
});
The throttled version guesses at a millisecond interval (16ms, aiming for roughly 60fps) that may not actually line up with when the browser is going to paint. The rAF version doesn't guess — it asks the browser directly for "right before your next paint," which is exactly when a visual update needs to be ready, no earlier and no later.
requestAnimationFrame doesn't need that guess — it's handed the actual right moment by the browser, which is why it's the better default for anything touching the visual output of the page, not just an arbitrary "smooth-feeling" alternative.input handler that fires a search API call has nothing to do with paint timing; debounce is still the right tool there, not rAF."This scroll handler is janky — what would you change?" is a real, practical question, and the strong answer isn't just "add throttle" — it's recognizing that a visual update tied to scroll position is specifically a requestAnimationFrame case, and explaining why that's a better fit than an arbitrary millisecond interval.