DEV Community

Cover image for Better event throttle
Eckehard
Eckehard

Posted on

Better event throttle

Sometime you need to make your code slower to make it faster...

Usually we want our code execute as fast as possible. But in case of DOM events, it can be useful to limit execution rate (see this post). Some events like window scroll may fire with a very high frequency, which may lead to a poor user experience or even blocking of the event chain.

You will find numerous implementations of throttle functions, the most common is presented here. Usually throttle executes the first event and then limits the rate of repetition. But in a UI it may happen, that there is not a single event causing a state change, but a burst of events happening in a short time. You might have some coupled states that depend on each other. Firing the first event would propagate only the first state change, but the depending changes would be suppressed until the next update. To prevent this, you would use a "debounce" function, that waits until the state has settled before propagating the first event.

As the situation happens frequently, I found it helpful to combine both functions in an advanced throttle function:

function throttle(cb, delay = 10, rate = 50) {
  let wait = false
  let args, timeout
  const tf = () => {
    if (args == null) {
      wait = false
      clearTimeout(timeout)
    } else {
      cb(...args)
      args = null
      timeout = setTimeout(tf, rate)
    }
  }

  return (...arg) => {
    if (wait) {
      args = arg
      return
    }
    wait = true
    setTimeout(() => {
      cb(...arg)
      timeout = setTimeout(tf, rate)
    }, delay)

  }
}
Enter fullscreen mode Exit fullscreen mode

This version uses an initial delay before calling the event function. After that, it limits the throttle rate to a different value. After the last event has happened, the execution stops and the next event will be propagated after the initial delay again. The default values of 10 ms and 50 ms have been found the be nearly unnoticeable, but any other value may serve better in your case.

The throttle function can be used as a function wrapper, arguments provided to the result will be forwarded to the wrapped function (see example below)

  function showScroll(){
    out.textContent = "scroll-position = "+window.scrollY
  }
  window.onscroll =  throttle(showScroll,10,250)

  // function wrapper example
  function log(s){
     console.log(s)
  }
  const tlog = throttle(log,500,2000)
  tlog("This is a test")
Enter fullscreen mode Exit fullscreen mode

You can try out a working example here

Top comments (2)

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

I'm a big fan of using microtasks for this sort of thing; sometimes they don't provide a long enough delay, but they're a good default tool when you're not entirely sure how much of a delay you need.

In practice, the most common cause I've encountered of repeated event firing is simply that either the same bit of code, or subsequent and/or nested function calls all modify the same bit of state.

This generally means that by the end of the task, we're usually done with the state "for now", and will either be waiting for some asynchronous code to finish, or for new input to respond to.

Another way I've found to work very well for debouncing things like user input is to reset the delay with every new function call; meaning that nothing will happen for as long as new inputs (e.g. keypresses) keep happening, and once the inputs stop or slow down, the page will start working with what it has.

This works well for input because generally speaking, a user that is busy typing something won't care if nothing is immediately happening, but will expect reasonably quick feedback once they stop. For these cases, a resetting delay of ca. 200ms feels to me like a good compromise between responsiveness and random changes while typing, which can feel like an interruption at times.

Collapse
 
efpage profile image
Eckehard

reset the delay with every new function call

This is done often, but was exact the reason to think about a different approach. If nothing happens until the user stops typing, this will be a bad experience. Think of a text input device, where you do not see any input until you stop typing...

Using a shorter initial delay makes your app feel responsive, but the repetition might be slower. This is exactly what I wanted to achieve.