Debouncing and throttling - sounds like something you'd do to a rubber duckie! But they're actually something you do with event handlers!
They're incredibly useful and this article shows you how to use them in Fantom. Also see the live demo below!
A problem or two you may have encountered before...
You create a Window resize event handler to re-calculate some complicated layouts when the Window size changes. Only when the user resizes the Window, the OS fires a gazillion resize events every second! No matter how much you optimise the layout code, trying to relayout everything a gazillion times a second maxes the CPU out at 99% and gives the user nothing but a sluggish and laggy experience.
Pah!
Or you put an text modified event handler on a search box so you can query the server for suggestions based on what the user is typing. It may work great for your boss or Great Aunt who are slow single finger typers, but your speed typing 69 words-per-minute colleague regularly tells you your website is slow and unresponsive! And he doesn't appreciate your suggestion of typing slower so as to not overload the server either!
Double pah!
Well, debounce and throttle to the rescue!
The debounce()
and throttle()
methods (Fantom code below) allow you to rate-limit your functions in multiple useful ways.
Pass a function in, and get a function out. You then use the returned function in place of the original. The new function may be called as frequently as you want, but the original function is only invoked according the debounce / throttle rules.
Visualisation
Not sure what the difference is between throttling and debouncing? Then see it visually in the real example below!
Hovering over the Fanny will fire an event every 50ms, that is then rate limited in various ways. You can then see which events get processed and when.
Example
Experiment by hovering on and off over Fanny and see when the debounced and throttled functions are actually executed.
Note this is a real example executing real Fantom code!
Debouncing
Debouncing coalesces bursts of function calls into just one. This single function execution may be called at either the end (the default) or at the beginning of the call burst.
This can be useful for resize and mousemove events as your event handler will only get called once the user has stopped resizing, or moving!
eventHandler := |Event event| { ... } debouncedFn := Debounce.debounce(200ms, eventHandler) elem.onEvent("resize", true, debouncedFn)
Throttling
Throttling ensures the original function is only called every XXms. Should the burst of function calls stop, then the original function is invoked one last time to ensure it always has the latest values.
Throttling can be useful for rate limiting the execution Ajax calls to a server, like the search box example.
eventHandler := |Event event| { ... } throttledFn := Debounce.throttle(200ms, eventHandler) elem.onEvent("change", true, throttledFn)
In terms of rate limiting function calls, throttling is always the first solution that comes to mind. But you should always stop and re-think. Because more often than not, debouncing is the solution that works better.
Code
Source code for the debounce()
and throttle()
methods, complete with documentation, can be found in the following BitBucket snippet:
But the inquisitive can view the important bits here. As you can see, the code is quite small!
using dom::Win @Js mixin Debounce {** Debounce a function.static Func debounce(Duration delay, |Obj?, Obj?, Obj?, Obj?| callback, Bool atBegin := false) { _throttle(delay, callback, null, atBegin) }** Throttle a function.static Func throttle(Duration delay, |Obj?, Obj?, Obj?, Obj?| callback, Bool noTrailing := false) { _throttle(delay, callback, noTrailing, null) } private static Func _throttle(Duration delay, |Obj?, Obj?, Obj?, Obj?| callback, Bool? noTrailing, Bool? atBegin) { throttling := noTrailing != null debouncing := atBegin != null// After wrapper has stopped being called, this timeout ensures that// 'callback' is executed at the proper times in 'throttle' and 'end'// debounce modes.timeoutId := null as Int lastExec := -1 wrapper := |Obj? p1, Obj? p2, Obj? p3, Obj? p4| { exec := |->| { lastExec = Duration.nowTicks callback(p1, p2, p3, p4) }// If `atBegin` is true this is used to clear the flag// to allow future `callback` executions.reset := |->| { timeoutId = null } if (debouncing && atBegin && timeoutId == null) exec() if (throttling && lastExec == -1) exec() elapsed := lastExec > -1 ? Duration.nowTicks - lastExec : 0 if (timeoutId != null) Win.cur.clearTimeout(timeoutId) if (throttling) if (elapsed > delay.ticks) exec() else if (!noTrailing) timeoutId = Win.cur.setTimeout(delay, exec) if (debouncing) timeoutId = Win.cur.setTimeout(delay, atBegin ? reset : exec) } return wrapper.retype(callback.typeof) } }
Credits
debounce()
and throttle()
for Fantom are based on the JQuery throttle / debounce Plugin by "Cowboy" Ben Alman.
Visualisation inspired from Debounce and Throttle: a visual explanation.
Edits
- 15 December 2017 - Original article.