Hajime, the duck guy

Wednesday, September 13, 2017, by Hajime Yamasaki Vukelic

Here are two important techniques that will prevent users from bringing your app to its knees.

Your parallax is all wired up, and your autocomplete is juiced up. It’s time for a wild ride. Except that about 20 miles down the road, you get a speeding ticket. Well, more like the opposite of a speeding ticket. Your event handlers are creating a major traffic jam at the intersection of the Performance Street and Usability Avenue.

Event trigger rate (a phrase I’ve just made up) is the number of times your event listener gets invoked in a given time frame. For some events this is quite unimpressive (e.g., a mouse click). For some events it is moderate (e.g., the input event when user is typing really fast). And for yet other events they are through the roof (e.g., scroll and mousemove).

The high trigger rate can crush your app like no other thing. But it can also be controlled quite effectively, which is the topic we are going to cover in this article. We're going to talk about throttling and debouncing.

From fluffy to ugly and back

When your event listeners aren’t doing much, you won’t really notice. Your app purrs on like a kitten drinking milk. So fluffy! As soon as your listeners are doing a bit more than feeding feline companions, though, things get ugly.

Performing XHR requests, manipulating the DOM in a way that causes repaints, process large amounts of data, and so on, these are all things that you shouldn’t be doing at a very high rate. Yet when you are moving a mouse around, the event gets triggered at insane rates and your listener is run every single time.

There are two ways to bring this all under control: throttling and debouncing. Both technique reduce the trigger rate to optimize resource usage, so it’s a compromise between the resolution of tracking the user actions, and optimal resource usage.

Throttling

Throttling is a straightforward reduction of the trigger rate. It will cause the event listener to ignore some portion of the events while still firing the listeners at a constant (but reduced) rate.

Throttling is used when you want to track the user’s activity, but you can’t keep up with a high trigger rate because you’re doing some heavy-lifting each time the event is triggered. One very common example is scrolling where you want your interface to react in response to the scroll position (e.g., real and fake parallax effects, sticky menus, etc).

Throttling can be implemented several ways. You can throttle by the number of events triggered, or by the rate (number of events per unit time), or by the delay between two handled events. The last option is probably the most common and also relatively easy to implement, so we’ll show it here. You can work out the other two on your own.

Let’s first hear it in plain English:

  1. Take a function and the minimal timing between two events
  2. Return a stand-in function that will be throttled
  3. Whenever it fires, the stand-in function will set a flag and start a timer that clears the flag after the minimal timing
  4. When the flag is set, the stand-in doesn't do anything
  5. When the flag is not set, the stand-in function calls the actual function

The example code will be a (completely usable) helper function that will convert any function into a throttled version.

function throttled(delay, fn) {
   var throttled = false
   return function (...args) {
      if (throttled) return
      throttled = true
      setTimeout(function () {
         throttled = false
      }, delay)
      return fn(...args)
   }
}

You can use it like so:

function handler(ev) {
   // The usual stuff
}

domNode.addEventListener('mousemove', throttled(200, handler))

What will happen is that the mouse event on the domNode node will only be able to trigger the myHandler function once every 200ms. All the events that happen meanwhile will be ignored.

Debouncing

Unlike throttling, debouncing is a technique of keeping the trigger rate at exactly 0 until a period of calm, and then triggering the listener exactly once.

Debouncing is used when you don’t need to track every move user makes as long as you can make a timely response. A common example is a widget that reacts to user typing. We normally assume that users will not care about what goes on the screen while they are typing, but want to see the result as soon as they are done. This assumption is (ab)used to, for example, reduce the number of XHR requests we make to obtain autocompletion candidates and thus conserve server resources.

Here it is in plain English:

  1. Take a delay in milliseconds and a handler function
  2. Return a stand-in debounced function
  3. When the stand-in is invoked, schedule the original listener to be invoked after the specified delay
  4. When the stand-in function is invoked again, cancel the previously scheduled call, and schedule a new one after the delay
  5. When calls to the stand-in function do not happen for a while, the scheduled call to the listener will finally go through

The example code is a helper that can be used to convert any function into a debounced one:

function debounced(delay, fn) {
   var timerId
   return function (...args) {
      clearTimeout(timerId)
      timerId = setTimeout(function () {
        fn(...args)
      }, delay)
   }
}

Note that, unlike the throttled(), the debounced() helper executes your callback function asynchronously. Therefore, return value of your callback is ignored. (E.g., you can't return false to prevent the default behavior.)

You can use this like so:

function handler(ev) {
  // The usual stuff
}
domNode.addEventListener('input', debouce(300, handler))

As the user types, the input event will get ignored until the user stops typing for 300ms. When 300ms since the last keystroke (or some other input action) elapses, the event listener will finally be triggered.

Unlike throttling, debouncing can be used more generally when we want to ensure that user is not interrupted while performing a task that requires focus (usually typing). Debouncing mutes the event listeners until user is seemingly done, so it can serve to prevent distraction until the user is done. We could, for example, use this to debounce validation errors or success messages.

Just like throttling, the timing is important. You want to control the trigger rate, but at the same time you want to respond fast when user is actually finished.

Demo

Move the mouse cursor around the demo area to see the effect. (Apologies to the keyboard and touch device users.)

Don't overdo it

As with any optimization technique, throttling and debouncing address a specific issue — event trigger rate. They are not supposed to be go-to solutions for all UI performance issues, and they won’t magically fix crappy code. You should always first investigate the source of the slowness and address other issues before attempting the trigger-rate-reduction fix.

For example, debouncing or throttling an unresponsive click event handler won’t do much because the issue is almost guaranteed to be unrelated to the trigger rate. Conversely, not all high-trigger-rate events need to be throttled or debounced. For example, an event listener used for driving a slider widget will not be able to do its job if it’s throttled — the result will be unusable to the end user.

Having said all this, these fixes are cheap, so if you’re out of ideas, they’re worth a try.

Posted in Programming tips
Back to top