Debounce, throttle and timers
Slow down fast events with debounced refs and throttled functions, and run timers that stop themselves.
What you'll learn
When you finish, you can do these.- 1Choose debounce or throttle for a burst of events
- 2Debounce a search box with refDebounced()
- 3Throttle pointer moves and tick a clock once a second
- 4Spot and fix a debounce that saves on every key
Picture a shop's search box that asks the server for matching products on every keystroke: typing "mug" sends three requests, and only the last one matters. Some events fire even faster. pointermove, the event the browser sends each time a pointer or a finger moves, arrives dozens of times a second.
VueUse, a library of ready-made functions for Vue, slows such events down in two ways, debounce and throttle, and adds timers that stop by themselves. The Code tab holds a page built with them: a clock, plus a product search, a note box and a pad that each count their work. By the end you can explain every number on it. From the lesson Meet VueUse, you need one idea: a VueUse function called in <script setup> hands you refs and cleans up after itself, as useEventListener() removed its listener when that lesson's mouse readout left the page. A ref is a box that holds a value and tells Vue when it changes.
Debounce waits, throttle paces
Both tools call your function fewer times than the events fire. Which calls they keep decides the job each one fits.
To debounce is to wait until the events stop for a set time, then run once with the latest value. Every new event restarts the wait. To throttle is to run at most once per interval while the events keep coming.
keykeykeykeypauseno event for the wait time
wait…every new event restarts the timerone callafter the pause
callfirst eventskipinside the intervalcallnext intervaltrailing callthe last event, when trailing is on
Debounce when only the final value matters: a search request, an autosave. Throttle when you want steady updates during the burst: a scroll progress bar, a drag, a pointer trail.
A search box that waits
The search box under the clock, SearchBox.vue in the Code tab, waits for a pause in your typing before it asks for results. It filters six products and counts the requests a real app would send.
refDebounced(source, ms) returns a second ref that follows the source, but only after ms milliseconds without a change. Here the source is query, the live ref: v-model writes every keystroke into it, so letters appear as you type. The slow copy is settled.
Vue's watch() runs a function with the new value each time a ref changes. It watches settled, so each run counts one request and filters the list:
const query = ref('')
const settled = refDebounced(query, 400)
watch(settled, (q) => {
requests.value++ // a real app would call the server here
results.value = products.filter(p => p.toLowerCase().includes(q.trim().toLowerCase()))
})
Type mug quickly in the preview. About 400 ms after your last key, Requests sent goes up by one, not three, and the list shows the three mugs.
Someone who types without a pause sends no request: every key restarts the wait. watchDebounced(), a watch() that waits, replaces refDebounced() and watch() in one call, and its maxWait option caps the wait. In SearchBox.vue it would look like this, with the body cut to its counter line:
watchDebounced(query, (q) => {
requests.value++
}, { debounce: 400, maxWait: 2000 })
With a debounce wait of 400 ms and a maxWait of 2000 ms, a long burst of typing still sends a request every 2 seconds, and one more after the last key.
Throttle, and timers that stop themselves
The two ends of the page show the other tools: a throttled pad at the bottom, a clock at the top.
The dashed pad, PointerPad.vue in the Code tab, counts pointer events and throttled calls. useThrottleFn(fn, ms) takes your function and returns a throttled one, track, that runs at most once every 300 ms:
const track = useThrottleFn(() => {
calls.value++
}, 300)
On every pointermove, the pad's handler, onMove, adds one to events and calls track. Sweep across the pad and you get dozens of events but only a few throttled calls.
The trailing call is on by default: the last event in a burst gets one more call when its interval ends, so a final position is never lost. Events at 0, 100, 200 and 350 ms give three calls, at 0, 300 and 600 ms.
To turn it off, pass false as the third argument: the settings are positional, known by their place in the call, with no options object. With useThrottleFn(fn, 300, false), the same four events give two calls, at 0 and 350 ms.
The clock at the top of the preview needs a timer. The browser's setInterval runs a function at a fixed interval until you clear it, even after its component has left the page. useIntervalFn(cb, ms) is setInterval with pause(), resume() and an isActive ref, and it clears itself when the component leaves the page.
The clock, in App.vue, prints now, a ref that useNow() fills with the current date and time. By default it gets a new value on every animation frame, each time the browser draws the screen: about 60 times a second. Each value makes Vue redraw the component that shows it. A clock needs one update a second, so give it a scheduler, a function that receives the update as cb and decides when to run it:
const now = useNow({ scheduler: cb => useIntervalFn(cb, 1000) })
The time on the page changes once a second, and the interval stops when the component goes.
What goes wrong
Two mistakes make a debounce useless: one starts a new timer on every key, the other never lets the typing through.
The note box under the search box, NoteBox.vue in the Code tab, autosaves and counts each save. useDebounceFn(fn, ms) is the debounce twin of useThrottleFn(): it returns a debounced copy of the save function. The first mistake creates the debounced function inside onInput, the handler that runs on every keystroke:
function onInput() {
useDebounceFn(() => {
saves.value++
}, 800)()
}
Type hello and Saves reads 5. Each keystroke made a new debounced function with its own timer, so no timer was ever restarted. Create the function once, while <script setup> runs, and call that one function from the handler.
const save = useDebounceFn(() => {
saves.value++
}, 800)
function onInput() {
save()
}
The Code tab has this version: typing hello adds one save, 800 ms after the last key.
The second mistake binds the search input to the debounced ref.
<input v-model="settled" />
The box shows your letters, but every keystroke warns [Vue warn] Set operation on key "value" failed: target is readonly. and Requests sent stays at 0. The debounced copy is read-only: it only follows its source. Bind the input to the live ref and debounce what depends on it.
<input v-model="query" />
Your letters reach query, and the request follows 400 ms later.
What to remember
- Debounce runs once after the events stop; throttle runs at most once per interval while they continue.
- Bind inputs to a live ref and debounce the work that reads it, with
refDebounced()orwatchDebounced(). AddmaxWaitwhen a long burst must still produce results. useThrottleFn()takes positional arguments;useThrottleFn(fn, 300, false)drops the trailing call.useNow()updates every frame by default. Pass an interval scheduler for a clock.- Create a debounced or throttled function once in
<script setup>, never inside the handler that calls it.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- In SearchBox.vue, change 400 to 1000 and type
travelat a steady pace. Requests sent rises by one, a full second after your last key. Clear the box, wait, then typecapwith a two-second pause after each letter: the count now rises after every letter. - Replace
refDebouncedand thewatchwithwatchDebounced(query, (q) => { … }, { debounce: 400, maxWait: 2000 }), keeping the body, and import it from@vueuse/core. Type without stopping for five seconds: Requests sent goes up about every 2 seconds while you type, then once more after you stop. - In NoteBox.vue, replace
save()inonInputwithuseDebounceFn(() => { saves.value++ }, 800)(). Typehello: Saves reads 5. Putsave()back and typehelloagain: Saves reads 1.