Debouncing and Throttling

Debouncing

If an event is occurring continuously, (scroll, typing, resize, ...) we call the function once either when the event first started or when it ended and will only call it again, after some interval of inaction of the event, followed by the event occurring again.

Implementation :

debounce.js
function debounce(cb, delay = 250) {
  let timeout
 
  return (...args) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      cb(...args)
    }, delay)
  }
}

The debounce function restarts the timer, if the event is triggered again

// call the function when the user types something, but call it only once the user has stoped for atleast a second
const updateOptions = debounce((query) => {
  fetch(`/api/getOptions?query=${query}`)
    .then((res) => res.json())
    .then((data) => setOptions(data))
}, 1000)
 
input.addEventListener('input', (e) => {
  updateOptions(e.target.value)
})

Another example

// A sample function that takes two arguments and logs them
function logArgs(arg1, arg2) {
  console.log(arg1, arg2)
}
 
// Create a debounced version of logArgs
const debouncedLogArgs = debounce(logArgs, 500)
 
// Call the debounced function with arguments
debouncedLogArgs('Hello', 'World')
Practical Example
index.html
<button>Fetch Users</button>
<ul id="users"></ul>
main.js
import './style.css'
 
const button = document.querySelector('button')
 
function debounce(func, wait) {
  let timeout
 
  return function (...args) {
    clearTimeout(timeout)
    timeout = setTimeout(() => func(...args), wait)
  }
}
 
async function fetchUsers() {
  console.log('calling the API')
  const ul = document.querySelector('#users')
  ul.innerHTML = ''
  const url = 'https://jsonplaceholder.typicode.com/users'
 
  try {
    const response = await fetch(url)
    const data = await response.json()
 
    data.forEach((user) => {
      const li = document.createElement('li')
      li.innerHTML = `${user.name}, ${user.email}`
      ul.appendChild(li)
    })
  } catch (error) {
    console.log(error)
  }
}
 
button.addEventListener('click', debounce(fetchUsers, 300))

There is one issue in the above code: If you want to add debouncing to search function you will need to add a cancel property to the returned function from the main function. So that you can use it in clean up function of say useEffect.

Updated code:

export default function debounce(cb, delay = 1000) {
  let timeout
 
  function debounced(...args) {
    clearTimeout(timeout)
 
    timeout = setTimeout(() => {
      cb(...args)
    }, delay)
  }
 
  debounced.cancel = () => {
    clearTimeout(timeout)
  }
 
  return debounced
}

For detailed example : https://github.com/adesh02092000/animalFarm/

Throttling

An event occurs, call the function and then wait for some interval, (no matter what happens in this period), after the delay if the event occurs again call the function again.

Implementation

function throttle(cb, delay = 250) {
  let shouldWait = false
 
  return (...args) => {
    if (shouldWait) return
 
    cb(...args)
    shouldWait = true
    setTimeout(() => {
      shouldWait = false
    }, delay)
  }
}
 
// call the throttle function instead of debounce
const updateOptions = throttle((query) => {
  fetch(`/api/getOptions?query=${query}`)
    .then((res) => res.json())
    .then((data) => setOptions(data))
}, 500)
 
input.addEventListener('input', (e) => {
  updateOptions(e.target.value)
})
Practical Example
function throttle(callback, delay) {
  let shouldWait = false
 
  return function (...args) {
    if (shouldWait) return
 
    shouldWait = true
    callback(...args)
    setTimeout(() => (shouldWait = false), delay)
  }
}
 
async function fetchUsers() {
  console.log('calling the API')
  const ul = document.querySelector('#users')
  ul.innerHTML = ''
  const url = 'https://jsonplaceholder.typicode.com/users'
 
  try {
    const response = await fetch(url)
    const data = await response.json()
 
    data.forEach((user) => {
      const li = document.createElement('li')
      li.innerHTML = `${user.name}, ${user.email}`
      ul.appendChild(li)
    })
  } catch (error) {
    console.log(error)
  }
}
 
button.addEventListener('click', throttle(fetchUsers, 1000))

Note: The main difference between debouncing and throttling is that, in case of debouncing if you keep clicking on the button, the timer restarts and nothing happens, (until there is an inactivity of the specified duration). But in case of throttling the button becomes active after the specified duration.

In simpler terms:

Debounce: user will decide when the function will be executed, by there inactivity.

Throttling: The button will be active again after say 1 sec, even if the user was clicking continuously. The idea is to maintain at-least the say 1 sec, delay between 2 function calls. The user activity does not determine when the button will be active

Explanation with analogy

In case of debouncing, consider the API calls to be a ball and Once you hit the server, now a wall is made and until you keep hitting the wall with the ball (API calls), it will debounce / reflect the ball and it won't reach the server. Only after you have not hit the wall for say 1 sec, the wall goes away and you can again hit the server, and again the wall will be created. It is mostly useful in case of auto suggestions

In case of throttling, once you make a API call, now a monster is between you and the server, and it will stay till say 1 sec, and if you make a API call in that time it will kill it. You can make the call after 1 sec, when the monster dies, but call the API will again bring the monster to life. Mostly used for protecting spam clicks.