Debounce vs Throttle Visualizer — Free JS Snippet

Debounce vs Throttle Visualizer · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real, correct debounce(fn, wait) implementation using a single cancel-and-reschedule setTimeout, not a simplified approximation
Real, correct throttle(fn, wait) implementation with both leading-edge and trailing-edge firing, so the last value in a burst is never dropped
Both lanes driven by one identical shared event source, so any difference in output is purely due to the timing algorithm, not different input
Marks positioned by real elapsed milliseconds against a fixed timeline window, not evenly spaced by event count
Live numeric fire counters per lane, making the core comparison explicit rather than only visual
"Fire 10 events quickly" burst control that reliably demonstrates the difference on demand, plus manual slider input for exploring the behavior at any speed
Distinct raw-tick vs. actual-fire mark styling (color, height, and a pop-in animation) so the two signal types are unambiguous at a glance
Self-cleaning timeline: old marks are automatically removed once they scroll out of the visible window, keeping the DOM light

About this UI Snippet

Debounce vs Throttle Visualizer — Real Implementations Compared Side by Side on an Animated Timeline

Screenshot of the Debounce vs Throttle Visualizer snippet rendered live

Debounce and throttle are the two most commonly confused timing functions in frontend JavaScript, and most explanations of the difference are purely verbal — "debounce waits for a pause, throttle limits the rate" — which is accurate but forgettable, because you never actually see the behaviors happen. This snippet feeds a single shared stream of rapid-fire events into two lanes at once, each wrapping the exact same raw event with a real, correct implementation of debounce or throttle, and plots every raw event as a small tick alongside a taller, differently colored mark for every actual function fire. Run the same burst through both lanes and the fire count difference — debounce firing once, throttle firing several times — becomes something you watch happen, not something you have to take on faith.

The debounce implementation, and why it has to clearTimeout on every call

debounce(fn, wait) closes over a single timer variable. Every time the returned wrapped function is called, it unconditionally calls clearTimeout(timer) before scheduling a new setTimeout. This is the entire mechanism: a rapid burst of ten calls 35ms apart never lets any of the first nine timers survive long enough to fire, because each new call cancels the previous pending timer before it can complete. Only once 300ms pass with *no new call arriving to cancel it* does the very last scheduled timer actually run. This is why the visualizer's debounce lane shows a cluster of raw ticks tightly packed together, followed by exactly one green fire mark sitting 300ms after the *last* tick in the cluster — not 300ms after the first.

The throttle implementation, and the trailing-edge detail most simplified versions skip

throttle(fn, wait) is more involved than debounce, and this is deliberate: a naive throttle that only checks "has enough time passed since the last fire" and otherwise does nothing will silently drop the final value of a burst if the burst ends mid-window. This implementation tracks lastFireTime and computes remaining = wait - (now - lastFireTime) on every call. If no time remains, it fires immediately (the "leading edge") and updates lastFireTime. If time does remain, instead of just discarding the call, it stores the latest args as pendingArgs and — only if no trailing timer is already scheduled — schedules one to fire exactly when the window ends (the "trailing edge"), using the *most recent* arguments captured by that point. This guarantees the throttled function always reflects the latest input by the time the window closes, rather than potentially firing with stale, outdated arguments from earlier in the burst.

Why the two lanes share one event source

Both lanes are driven by the exact same onRawEvent() function, called once per raw slider input event or once per tick of the "Fire 10 events quickly" burst. Every single raw event is passed to *both* debouncedFn() and throttledFn() in the same call. This matters for the comparison to be honest: if the two lanes were driven by separately generated random event streams, any difference in fire count could be attributed to different input rather than different timing behavior. By construction, both wrapped functions here see literally identical input, so the entire difference in how many times each one fires is attributable purely to the debounce-versus-throttle logic itself.

Plotting real elapsed time, not just counting events

Every mark's horizontal position is computed from xPercentForNow(), which measures real elapsed milliseconds since the timeline was last reset against a fixed LANE_MS window (6 seconds), not from a fixed per-event pixel spacing. This means the visual gap between the raw ticks in a burst and their eventual fire mark(s) is an honest representation of the actual 300ms wait — if you fire the burst slowly by hand instead of using the button, the ticks visibly spread further apart and the debounce fire mark visibly follows further behind the last one, exactly matching the real timing.

Why the fire count is the crux of the whole comparison

For a burst of ten rapid raw events, debounce ends the burst having fired its wrapped function exactly once — after the last event plus 300ms. Throttle, wrapping the identical burst, fires near the very start (leading edge) and, if the burst duration exceeds the 300ms window, potentially a second time at the trailing edge — always at least once, and typically more than debounce's single fire for the same input. That fire-count difference for identical input, plotted side by side with visibly different-colored taller marks, is the entire point of the snippet: it is not asserted in prose, it is counted and displayed live in each lane's header.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Give this snippet's JS to an AI assistant like Claude and ask it to trace through why throttle() needs both a lastFireTime check and a separate pendingArgs/trailing timer, rather than just checking "has enough time passed" — the trailing-edge case is the detail most simplified explanations of throttle skip entirely, and seeing it implemented correctly is worth more than another verbal description. It's also worth asking how you'd implement a debounce variant with a maxWait option (guaranteeing a fire even under continuous, uninterrupted input, similar to lodash's implementation) and what that would add to the visualization. For extending the snippet: ask for a third lane showing raw, unthrottled/undebounced fires for full comparison, a live-adjustable WAIT slider so the timing window itself can be tuned interactively, or exporting the fire-time data as a downloadable timeline for a blog post or presentation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a side-by-side visual comparison of debounce versus throttle in plain HTML, CSS, and JavaScript, no libraries, using real correct implementations of both.

Requirements:
- A single shared, fast-firing input source: a slider the user can drag, plus a "Fire 10 events quickly" button that simulates a rapid burst of events roughly 30-40ms apart, well inside a shared wait window.
- Implement a real, correct debounce(fn, wait) function: every call must clear any pending timer and reschedule a new one, so the wrapped function only actually fires once, after wait milliseconds have passed with no further calls arriving.
- Implement a real, correct throttle(fn, wait) function that fires immediately on the first call (leading edge) and at most once per wait-millisecond window after that — but must NOT silently drop the final call of a burst that ends mid-window; it must track the most recent call's arguments and fire once more at the trailing edge if a call was suppressed during the window.
- Feed the identical burst of raw events into both the debounced and throttled wrapped functions at the same time, so any difference in behavior is attributable purely to the algorithm, not to different input.
- Visualize both lanes as horizontal timelines: plot every raw incoming event as a small tick mark, and separately mark (in a different color and taller) exactly when each lane's wrapped function actually fires, with mark positions reflecting real elapsed time, not a fixed per-event spacing.
- Display a live numeric fire count per lane so the difference in how many times each function actually ran for the identical burst is explicit, not just visual.
- Debounce must visibly show a single fire happening only after the burst has fully stopped; throttle must visibly show more fires than debounce for the same burst, including a fire near the very start of the burst.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Drag the slider slowlyBoth lanes plot a light purple tick for every raw input event, spaced out in real time to match how quickly you actually moved the slider.
  2. 2
    Click "Fire 10 events quickly"Ten raw events fire about 35 milliseconds apart, well within the shared 300ms window, landing a tight cluster of ticks on both timelines at once.
  3. 3
    Watch the Debounce laneA single taller green "fire" mark appears roughly 300ms after the very last tick in the cluster — every earlier pending timer got cancelled by the next incoming event before it could run.
  4. 4
    Watch the Throttle lane at the same timeA green fire mark appears almost immediately at the start of the burst (the leading edge), and depending on how long the burst runs, a second fire mark appears at the trailing edge — visibly more fires than the debounce lane for the identical input.
  5. 5
    Compare the two "fires" counters in the lane headersThe numeric counters make the difference explicit: debounce typically reads 1 fire for a quick burst while throttle reads 2, proving the core distinction directly rather than just describing it.
  6. 6
    Click Reset and try a slow, spread-out drag instead of the burst buttonWith gaps longer than 300ms between events, debounce fires after every individual pause and throttle fires close to every event too — the two behaviors converge and look similar, which itself demonstrates that the difference only shows up under rapid, bursty input.

Real-world uses

Common Use Cases

Search-as-you-type and autocomplete inputs
The classic debounce use case — delay an API search request until the user actually stops typing, avoiding a request per keystroke; compare against the autocomplete input snippet where this exact debounce pattern is applied in a real search field.
Scroll and resize event handlers
The classic throttle use case — limit how often an expensive scroll-position or window-resize handler actually runs, capping it to a fixed rate instead of firing on every single native event, which can fire dozens of times per second.
Teaching the debounce vs throttle distinction visually
A concrete, side-by-side reference for the single most commonly confused pair of timing utilities in frontend interviews and real codebases, pairing the correct implementations with a live, undeniable proof of their differing fire counts.
Drag sliders and range input performance tuning
Directly demonstrates the tradeoff involved in deciding whether a slider's onChange handler (updating a live preview, chart, or price calculation) should be debounced or throttled, using this exact range slider style input as the event source.
Engineering documentation and onboarding material
A ready-made interactive diagram for internal engineering docs or a frontend fundamentals training deck, replacing a static explanation with something a new hire can actually manipulate and watch.

Got questions?

Frequently Asked Questions

Debounce fires the wrapped function only once, a fixed delay after the last event in a burst — every new event cancels and reschedules the pending timer, so rapid-fire input keeps pushing the fire time later until the input actually stops. Throttle fires immediately on the first event and then at most once per fixed window afterward regardless of how many more events arrive, so a burst of events lasting longer than the window produces multiple fires. Feeding an identical burst into both (as this snippet does) makes the resulting fire-count difference directly observable rather than something you have to take on faith from a description.

A throttle that only fires on the leading edge would silently ignore the final state of a fast-moving input if the burst ends inside the wait window — for example, a slider dragged quickly to its final value could have that final value never actually get processed if the last raw event happened to land inside the throttled window. This implementation stores the most recent call's arguments as pendingArgs and schedules exactly one trailing-edge fire for when the window closes, guaranteeing the throttled function is eventually called with the latest input, not a stale mid-burst value.

For a burst of ten events fired within a single 300ms window, debounce collapses the entire burst into exactly one fire, 300ms after the last event. Throttle, wrapping the same events, fires once immediately when the burst starts and then, if the burst duration crosses the 300ms boundary, fires again at the trailing edge — meaning throttle will always produce at least as many fires as debounce for the same bursty input, and typically strictly more, which is exactly the difference the two lanes are built to make visible.

Yes, but they must be created once and persisted across renders rather than recreated on every render, or their internal timer/lastFireTime state resets constantly and the behavior breaks. In React, wrap the debounce/throttle call in useMemo or useRef so the same closure instance persists across re-renders, and clear any pending setTimeout inside a useEffect cleanup function on unmount. In Vue, create the wrapped function once outside the reactive setup (or in onMounted, stored in a non-reactive variable) and clear its timer in onUnmounted. In Angular, create it once in the component class (not inside a method that runs on every change detection cycle) and clear it in ngOnDestroy.

Change the WAIT constant at the top of the script — both debounce(fn, WAIT) and throttle(fn, WAIT) read from the same value, so the two lanes stay directly comparable at any window size. To test a different burst pattern, edit the interval passed to setInterval inside fireBurst() (currently 35ms between each of 10 events) — try spacing events further apart than WAIT to see debounce and throttle converge and fire similarly, which itself demonstrates that their difference only becomes dramatic under genuinely rapid, bursty input.