Have you ever experienced your page getting very slow when you scroll down or run some animation?
When I used to work on the Canvas animation for dashboards, it required a lot of CPU resources just to represent the animation every second smoothly. Then one day, a new requirement was added: The tooltip should be shown once your mouse cursor hovers over the Canvas. This was really killing me. My CPU’s resources were so depleted that my computer was kind of frozen.
After a while, I learned a workaround for this problem: throttle or debounce. So in this article, I want to share what they are and why they are useful.
If you know how JavaScript’s asynchronous functions and HOF work under the hood, you could accept this theory more easily. You might be interested in my other articles about JavaScript events and the HOF.
Let’s say you want to search a country name through the API call. There’s an input box on the screen in which you can put the strings of the country name. Once you put the strings in it, your application hits the API call to load all the countries that include your strings.
Here’s a short example made by CodeSandBox:
Try to search for any country.
For mobile users or lazy people like me, here’s what happens if you look for the countries:
Request Count
on the screen refers to how many HTTP requests you have sent to the server. Note that it’s increased every time you press the keyboard, including the backspace. This looks OK, but imagine the normal size of an application you work on or a big application such as Airbnb.
However, sending requests every single time from the keyboard or any user action is certainly too much. We need a workaround for this.
To understand what they really are, I think it’s better to tell you the idea behind the solution to the problem above.
The main problem was too many HTTP requests were sent. Let’s say you wanted to search “Canada.” But if your web page is kind of frozen due to the requests for “C,” “Ca,” “Can,” “Cana,” etc. I’m sure nobody would be happy. So the demand for reducing the requests came into the world.
Debounce and Throttle are just names for how you actually reduce the requests. The common thing between Debounce and Throttle is a simple concept: Once my finger presses the keyboard, don’t send any requests for a while until I say it’s OK.
Photo by the author.
Look at the figure above. It represents what happens, depending on the cases. Normal
sends a request every time. Throttle
sends a request the first time during the period and never sends another request until the period is over. Once the period is over, it sends a new request again. Debounce
takes a callback that will be invoked in milliseconds and only sends a request as long as no extra requests are added during the period. Look at the case when “n” of “Canada” is pressed. An extra request is added and the previous request added when “a” was pressed is now going to be ignored. With this logic, if all the requests were added within the period (some milliseconds), only the last one will be executed.
OK, now let’s explore the code!
import axios from 'axios';
const search = async (city) =>
await axios.get(`https://restcountries.eu/rest/v2/name/${city}`)
search
is the function that sends the city using the sample city API. I used the real restful API for the test to make this example more realistic. All you need is to fill out city
with whatever the city is. Try it out yourself.
Let’s check out the code first:
const throttle = (delay, fn) => {
let inThrottle = false;
return args => {
if (inThrottle) {
return;
}
inThrottle = true;
fn(args);
setTimeout(() => {
inThrottle = false;
}, delay);
};
};
medium_throttle.js
This anonymous function is a HOF that returns another function. When this function is called for the first time, inThrottle
is assigned to false
. Then once the returned function is called again, inThrottle
is set to true
and the callback function (fn
) is executed. Then inThrottle
becomes false
again in delay
ms. In the meantime, while inThrottle
is true
, none of the execution of the returned function from throttle
can be executed.
Do you remember this? Even if you keep clicking the button, for example, Throttle
doesn’t allow you to execute the function in a row unless inThrottle
is false
.
Then the new method for requesting would be like this:
const sendRequestThrottle = throttle(500, search);
<input type="text" onChange={sendRequestThrottle} />
But, there’s one thing you need to be careful about.
The HOF method throttle
should not be included in the React component. When state variables that are in component A are changed, A is re-rendered and everything is re-assigned again. If throttle
stays re-rendered, inThrottle
will also be re-assigned to false
.
Now let’s look at the Debounce
. This code is quite similar to Throttle
’s, so you should understand this part easily as well:
const debounce = (delay, fn) => {
let inDebounce = null;
return args => {
clearTimeout(inDebounce);
inDebounce = setTimeout(() => fn(args), delay);
}
}
medium_debounce.js
That’s all. It’s super simple. The only difference with Throttle
is that Debounce
doesn’t check if inDebounce
is true
or whatever. If the callback is executed within a certain time period, it cancels the previous setTimeout
that was going to be run soon and creates a new one. So if you keep pressing the keyboard fast enough, your callback would never be run.
Now you’ll understand much better why Debounce
’s calls look like the figure above.
All you need to do is to register your callback in the DOM:
const sendRequestDebounce = debounce(500, search);
<input type="text" onChange={sendRequestDebounce} />
In most cases, you wouldn’t need to make a Throttle
or Debounce
because there are so many good lightweight libraries out there for these features. Lodash is also one of them.
function debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime
let lastInvokeTime = 0
let leading = false
let maxing = false
let trailing = true
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId)
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
function pending() {
return timerId !== undefined
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
medium_loadsh_debounce.js
The code is quite long, but look at the debounced
function, which is a HOF:
function debounce() {
let lastCallTime;
...
function debounced(...args) {
...
}
return debounced;
}
debouncehof.js
debounce
returns a new function (debounced
) that is executed indeed. Inside debounced
, lastCallTime
is used. So you may notice that debounce
in Lodash
might compare the previous time (which the function was previously called) with the current time (which the function is currently called).
To make this strategy a bit simpler, the code might look like this:
const throttled = function(delay, fn) {
let lastCall = 0;
return (...args) => {
let context = this;
let current = new Date().getTime();
if (current - lastCall < delay) {
return;
}
lastCall = current;
return fn.apply(context, ...args);
};
};
medium_lodashlike_throttle.js
Well, you could create your own debounce
if you want, but just remember that the important thing is to postpone the function execution to the later point.
Debounce
and Throttle
both came from the need to delay the function execution because users didn’t want too many HTTP requests to be made. Nowadays, these are important ways to improve web performance. You can use either of them for any tasks that you want to delay, such as scrolling events.
#javascript #programming