You've reached for _.debounce a hundred times. You drop it around a search handler or a resize listener and the jank goes away. You have never had to write one. Over the next seven screens you will — and every wall you hit writing it naively turns out to be something the real implementation already solved. The walls show up in order; the fixes land only after you've felt the need.

Start here. Open your app's typeahead search and type a word — nine characters, maybe ten. The network panel lights up like a Christmas tree. Every keystroke has fired its own request: /api/search?q=j, /api/search?q=ja, /api/search?q=jav. Nine calls for one query. The responses come back out of order, so the UI flashes “Java” results, then "J“ results, then ”JavaScript" results. A spinner wouldn't help — by the time the right response lands, you've typed the next letter.

Signal source · search
type “javascript”
Requests fired0
429 rejections0
Backend cap5/sec
Request logempty
  • Start typing…

Each keystroke fires its own fetch — a round-trip with a request body, a JSON parse, and a state update on the way back. The request log on the right stacks up one row per character. Type “javascript” at normal speed and you'll see ten rows in under a second. Nothing in the handler knows that the user is MID-BURST; each call is a self-contained “search for this exact prefix.”

Worse: the first rows in the log flip green (status 200), but past a certain count they flip red (status 429 — rate-limited). Most public APIs cap the same token at a handful of requests per second. By the time the user finishes typing, the backend has already rejected half of the burst, and the UI — still bound to whichever response lands LAST — has no idea which result actually matches the query on screen.