Optimize Speed with Debounce

March 2017 ยท 3 minute read

One of the easiest optimizations that you can make to your code, if you have a function that gets called often (ex. key press event listener), is using debounce. The idea of debounce is that you store the data that you recieve from an event emitter, but you don’t act on it right away. You wait a predefined amount of time, then you run your callback function with the latest data.

An example of this is listening to input change of a text field. Every time the user types a character, your event listener gets fired. If your event listener is doing an expensive task, such as making an API call, then it’s very unnecessarily inefficient to send a request on every key stroke when you only care about the last. In most cases, it would be a much better solution to wait for the user to stop typing before you do your expensive job.

In the next example, we query the backend 100 milliseconds after the user stops typing:

inputField.oninput = debounce(100, queryBackend);

JavaScript

Here’s what the code for debounce would look like in JavaScript:

function debounce(callback, delay) {
  var timeout; // the time counter. It resets every time debounce gets called.
  return function() {
    // parameters to pass on to the callback function
    var context = this;
    var args = arguments;

    // call this after it stops getting called for the specified delay.
    var callAfterFiringStopsForDelay = function() {
      timeout = null; // clear the timeout pointer
      callback.apply(context, args); // call callback and pass its arguements to it
    };

    clearTimeout(timeout); // cancel callback if we get called before the delay ends

    timeout = setTimeout(callAfterFiringStopsForDelay, delay); // delay the callback
  }
}

Go

And here’s the debounce code in Golang:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// subscribeToEvents returns a channel from an empty interface.
// This allows us to accept any type of channel in the debounce function.
func subscribeToEvents(i interface{}) <-chan interface{} {
  ch := make(chan interface{})

  go func() {
    for {
      // Receive the value from the channel
      val, ok := reflect.ValueOf(i).Recv()
      if !ok {
        close(ch) // close the channel if the passed channel is closed
        return
      }
      // Convert the received value to interface and send it over the returned
      // channel
      ch <- val.Interface()
    }
  }()

  return ch
}

// Debounce runs the handler function after the eventEmitter channel stops
// sending events for the specified interval.
func Debounce(interval time.Duration, eventEmitter interface{},
  handler func(interface{})) {
  // Read events from eventEmitter
  events := subscribeToEvents(eventEmitter)

  // Wait to receive an event
  for event := range events {
  L:
    for {
      // If an event is received, wait the specified interval before calling the
      // handler.
      // If another event is received before the interval has passed, store
      // it and reset the timer.
      select {
      case event = <-events: /* Do nothing */

      case <-time.After(interval):
        handler(event)
        break L
      }
    }
  }
}

Check out the code demo on the Go playground.