Ulzurrun de Asanza i Sàez

Throttle and debounce visualized

throttle and debounce are two functions widely used in frontend applications but they are confusing and oftentimes used interchangeably even though they are not the same thing. In this article we’ll review both and clarify their differences (or go directly to the interactive demo showcasing the differences).

throttle and debounce are used to filter a stream of events so they are implemented in several front end libraries (but they can be handy in backend code, too). We’ll focus on the Lodash implementation (throttle: source code, documentation; debounce: source code, documentation).

debounce

A debounced function will be executed only after passing some time without being executed.

Imagine a search form. Whenever user types something we can perform a network request to fetch the search results. However, performing a network request after each character is typed is quite inefficient: user is interested only in the results of the last query, they don’t care about the intermediate queries results so why requesting them?

If we debounce the fetch_results function, we will perform the network request only after some time has passed without the user typing anything in the search form.

In this scenario we want to run the function after the time has passed but there are situations where we would like to run the function immediately and don’t run it again after some time passes.

Lodash support this with two optional parameters: trailing (the function should be run after some time without running the function) and leading (the function is run immediately and then it’s not run even if we call it again).

Both parameters can be enabled. In that scenario the function will be called immediately and if we run it again, it will be run one more time but only after passing some time without calling the function.

Stream of events as they happen and as they trigger debounced handlers

throttle

A throttled function will be executed immediately and start a timer during which it won’t be executed. The function will be executed again after the timer expires but only if the function was called during that period.

Imagine a function that adjusts the size of a container depending on viewport size. We can call this function whenever user resizes the browser window but resize event is triggered several times in a short period of time and we might end up taking longer to handle the resize event than it takes the resize event to happen. The user will experience a stumbling movement until they stop resizing the window.

Debouncing is not an option, either: the user won’t see any change until after they stop resizing the window.

However, if we throttle this function we will resize the container when user starts resizing the window, and again with a predefined frequency until they stop resizing the window.

Similarly to debounce, Lodash implementation of throttle supports a leading and a trailing parameters, allowing to set whether we want to execute the function at the beginning of the time period (leading), at the end of it (trailing) or both.

Stream of events as they happen and as they trigger throttled handlers

throttle vs debounce visualized

The demo below showcases a stream of events and how debounced and throttled handlers are called. Move the cursor over the Trigger area (or tap it if you are using a touch-capable device) to trigger an event.

<a class="trigger-area">Trigger area</a>
<a class="reset">Reset</a>
<div class="visualizations">
  <h2>Raw events over time</h2>
  <div id="raw-events" class="events"></div>

  <h2>Debounced events <span class="details"> 400ms, trailing</span></h2>
  <div id="debounced-events-trailing" class="events"></div>

  <h2>Debounced events <span class="details"> 400ms, leading</span></h2>
  <div id="debounced-events-leading" class="events"></div>
  
  <h2>Debounced events <span class="details"> 400ms, trailing and leading</span></h2>
  <div id="debounced-events-trailing-and-leading" class="events"></div>

  <h2>Throttled events <span class="details"> 400ms, trailing</span></h2>
  <div id="throttled-events-trailing" class="events"></div>

  <h2>Throttled events <span class="details"> 400ms, leading</span></h2>
  <div id="throttled-events-leading" class="events"></div>
  
  <h2>Throttled events <span class="details"> 400ms, trailing and leading</span></h2>
  <div id="throttled-events-trailing-and-leading" class="events"></div>
</div>
body {
   background: #444444;
   color: white;
   font: 15px/1.51 system, -apple-system, ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif;
   margin:0 auto;
   max-width:700px;
   padding:20px;
}

.events{
  padding:0px 20px 10px 20px;
  height: 23px;
}
.events span {
  height:17px;
  width:6px;
  display:inline-block;
  border-right:1px solid #111;

}
.events span:last-of-type {
  border:2px solid black;
  border-bottom: 4px solid #AAA;
  border-top: 0px;
  margin-bottom:-17px;
  margin-left:-2px;
}
h2 {
  margin:10px 0 5px 0;
  clear:both;
  font-weight: normal;
  font-size:14px;
  padding:6px 20px;
}

.trigger-area {
  margin: 0;
  display:inline-block;
  width: 200px;
  height:50px;
  border: 1px solid #5ed1ff;
  padding: 28px 0 0 0;
  text-align: center;
  background-color: transparent;
  cursor:pointer;
  font-size:17px;
  -webkit-user-select: none;  /* Chrome  / Safari */
  -moz-user-select: none;     /* Firefox all */
  -ms-user-select: none;      /* IE 10+ */
  user-select: none;          /* Likely future */    
}
.trigger-area.active {
  background:#2F5065;
}
.clickme:hover,
.clickme:active{
  background-color: #333;
}
.clickme:active{
  padding: 4px 5px;
}
.reset {
  display:inline-block;
  width: 120px;
  padding: 10px 0 0 0;
  text-align: center;
  font-size:14px;
  cursor:pointer;
  color:#eee;
}
.visualizations {
  margin-top:10px;
  background:rgba(0,0,0,0.2);
}
.details {
  font-size:13px;
  color:#999;
}

/* stating the obvious: color0 represents our empty color */
.color0 { transparent}

.color1 { background-color: #FFE589}
.color2 { background-color: #B9C6FF}
.color3 { background-color: #99FF7E}
.color4 { background-color: #FFB38A}
.color5 { background-color: #A5FCFF}
.color6 { background-color: #FF8E9B}
.color7 { background-color: #E3FF7E}
.color8 { background-color: #FFA3D8}
.color9 { background-color: #5ca6ff}
.color10 { background-color: #9BFFBB}
$(document).ready(function () {
  var $rawDiv = $("#raw-events"),
    $debounceTrailingDiv = $("#debounced-events-trailing"),
    $debounceLeadingDiv = $("#debounced-events-leading"),
    $debounceTrailingAndLeadingDiv =$("#debounced-events-trailing-and-leading"),
    $throttleTrailingDiv = $("#throttled-events-trailing"),
    $throttleLeadingDiv = $("#throttled-events-leading"),
    $throttleTrailingAndLeadingDiv = $("#throttled-events-trailing-and-leading"),
    $triggerArea = $(".trigger-area"),
    initialized = false,
    frequency = 100,
    barLength = 0,
    globalColor = 2,
    interval_id,
    rawColor = 0,
    debounceLeadingColor = 0,
    debounceTrailingColor = 0,
    debounceTrailingAndLeadingColor = 0,
    throttleLeadingColor = 0,
    throttleTrailingColor = 0,
    throttleTrailingAndLeadingColor = 0,
    maxBarLength = 64;

  var drawDebouncedLeadingEvent = _.debounce(
    function (div) {
      debounceLeadingColor = globalColor;
    },
    frequency * 4,
    { leading: true, trailing: false }
  );

  var drawDebouncedTrailingEvent = _.debounce(
    function (div) {
      debounceTrailingColor = globalColor;
    },
    frequency * 4,
    { leading: false, trailing: true }
  );
  
  var drawDebouncedTrailingAndLeadingEvent = _.debounce(
    function (div) {
        debounceTrailingAndLeadingColor = globalColor;
    },
    frequency * 4,
    { leading: true, trailing: true }
  );

  var drawThrottledLeadingEvent = _.throttle(
    function (div) {
      throttleLeadingColor = globalColor;
    },
    frequency * 4,
    { leading: true, trailing: false }
  );

  var drawThrottledTrailingEvent = _.throttle(
    function (div) {
      throttleTrailingColor = globalColor;
    },
    frequency * 4,
    { leading: false, trailing: true }
  );
  
  var drawThrottledTrailingAndLeadingEvent = _.throttle(
    function (div) {
      throttleTrailingAndLeadingColor = globalColor;
    },
    frequency * 4,
    { leading: true, trailing: true }
  );

  var changeDebouncedColor = _.debounce(
    function (div) {
      // Change colors, to visualize easier the "group of events" that is reperesenting this debounced event

      globalColor++;
      if (globalColor > 9) {
        globalColor = 2;
      }
    },
    frequency * 4,
    { leading: false, trailing: true }
  );

  function draw_tick_marks() {
    // every x seconds, draw a tick mark in the bar
    interval_id = setInterval(function () {
      barLength++;
      $rawDiv.append('<span class="color' + rawColor + '">');
      $debounceTrailingDiv.append(
        '<span class="color' + debounceTrailingColor + '">'
      );
      $debounceLeadingDiv.append(
        '<span class="color' + debounceLeadingColor + '">'
      );
      $debounceTrailingAndLeadingDiv.append(
        '<span class="color' + debounceTrailingAndLeadingColor + '">'
      )
      $throttleTrailingDiv.append(
        '<span class="color' + throttleTrailingColor + '">'
      );
      $throttleLeadingDiv.append(
        '<span class="color' + throttleLeadingColor + '">'
      );
      $throttleTrailingAndLeadingDiv.append(
        '<span class="color' + throttleTrailingAndLeadingColor + '">'
      );
      rawColor = 0; // make it transparent again
      debounceLeadingColor = 0; // make it transparent again
      debounceTrailingColor = 0; // make it transparent again
      debounceTrailingAndLeadingColor = 0; // make it transparent again
      throttleLeadingColor = 0; // make it transparent again
      throttleTrailingColor = 0; // make it transparent again
      throttleTrailingAndLeadingColor = 0; // make it transparent again

      if (barLength > maxBarLength) {
        clearInterval(interval_id);
      }
    }, frequency);
  }

  // Track Mouse movement or clicks for mobile
  $triggerArea.on("click mousemove", function () {
    if (!initialized) {
      initialized = true;
      draw_tick_marks();
      $(this).addClass("active");
    }
    rawColor = globalColor;
    drawDebouncedLeadingEvent();
    drawDebouncedTrailingEvent();
    drawDebouncedTrailingAndLeadingEvent();
    drawThrottledLeadingEvent();
    drawThrottledTrailingEvent();
    drawThrottledTrailingAndLeadingEvent();
    changeDebouncedColor();
  });

  $(".reset").on("click", function () {
    initialized = false;
    $triggerArea.removeClass("active");
    $rawDiv.empty();
    $debounceTrailingDiv.empty();
    $debounceLeadingDiv.empty();
    $debounceTrailingAndLeadingDiv.empty();
    $throttleTrailingDiv.empty();
    $throttleLeadingDiv.empty();
    $throttleTrailingAndLeadingDiv.empty();
    barLength = 0;
    clearInterval(interval_id);
  });
});

2 replies on “Throttle and debounce visualized

  1. Thanks for this cool.

    But I think your throttle image is not accurate. In trailing there should be at max 400ms gap between events. Isn’t it?

    1. That’s right!

      I took the screenshots from the demo and the demo has a race condition that sometimes delays the colored rectangle a frame.

      Sorry for that! I hope the rest of the article makes things clear.

Leave a Reply

Your email address will not be published.

Required fields are marked *

Your avatar