Issues with the throttle() function in Underscore.js and my throttle() fixes

Posted by Dan on Aug 18, 2011 @ 12:40 PM

[UPDATED: Wednesday, August 24, 2011 at 3:19:27 PM]

I was looking through the source code of Underscore.js this morning and notice it's implementation of _.throttle() contains two issues that bother me with most JavaScript-based throttle implementations I've seen:

  1. It delays the execution of the first hit until the "delay" has been reached. While this is ideal for debounce type operations, I believe throttle operations should execute immediately and then only execute after the delay for each additional call. Throttling works really well for mouse movement and scrolling operations, but you usually want the behavior to initiate when the operations begins—delaying the first execution until the delay is reach usually ends up with an odd behavior.
  2. The arguments passed to the throttled event are based upon the invoking function that triggered the setTimeout() event. This means you are not dealing with the latest data passed to your throttled event, but dealing with expired data. A perfect example of this is using throttling to monitor mouse movements. The way Underscore.js has implemented the throttle() function, you could end up with coordinates based on where the pointer started—not where it ended.

Let's take a look at an example I posted on JSFiddle: http://jsfiddle.net/MNGpr/

Mouse your mouse from the top left of the "Results" frame to the bottom right. If you do this quickly (under the 1 second delay,) you'll notice that when the function executes it's based upon coordinates in the upper left of the screen—not where your cursor left off.

What I've been doing is using the following throttle() function in my code:

// limit a function to only firing once every XX ms
var throttle = function (fn, delay, trail){
  delay || (delay = 100);
  var last = 0, timeout, args, context, offset = (trail === false) ? 0 : delay;
  return function (){
    // we subtract the delay to prevent double executions
    var now = +new Date, elapsed = (now - last - offset);
    args=arguments, context=this;

    function exec(){
      // remove any existing delayed execution
      timeout && (timeout = clearTimeout(timeout));
      fn.apply(context, args);
      last = now;
    }

    // execute the function now
    if( elapsed > delay ) exec();
    // add delayed execution (this could execute a few ms later than the delay)
    else if( !timeout && trail !== false ) timeout = setTimeout(exec, delay);
  };
};

What this version does that differentiates it from the Underscore.js version is it:

  1. Executes immediately upon first hit and then only after the specified delay. IMO, this provides a more desired effect for most throttled events. If you're really looking to do something when a user stops interacting with the screen, then use a debounce technique.
  2. The last arguments are applied when executing the throttled function. This means that the latest version of the values are used, instead of the values supplied to the initial timed event.

To see the difference in behavior, check out the JSFiddle example using my version of throttle().

Categories: JavaScript

13 Comments

  • Excellent!
  • Two things... I don't see the purpose of the outer closure, and the context for the function to execute in seems to get misplaced somewhere along the line. Setting an object's method to 'throttle(function(){...original method }, 250)' for example, won't work with this.

    Here's a fork of your fiddle that addresses that. http://jsfiddle.net/broberson/AY3cY/

    Thanks, Dan, for doing the heavy lifting on this one. Underscore's weird throttling behavior was getting on my nerves, and your take as to what it ought to be doing instead is spot on.
  • Meh. Forgot the fiddle 'version' part of the URL.

    http://jsfiddle.net/broberson/AY3cY/1/
  • @Brandon:

    Good catches. The outer closure isn't needed and was a remnants left over from some refactoring. I made the following changes:

    1) Declared a context variable (so it's not in the global space)
    2) Removed the named function from the return statement (there's no real need for it)
    3) Saved a reference to "this" in the context variable
  • PS - I've actually updated the code in the blog and on Fiddle with the changes.
  • Dan glad to see I'm not alone with this. How does your version differ from the patch I submitted? https://github.com/documentcloud/underscore/pull/2...
  • If I'm reading this code right, it makes an extra call to your throttled method which just doesn't seem right.

    For example, call the throttled method 2x in a row, then wait. The first call will happen immediately. The 2nd call will happen after 100ms (or whatever delay you use)

    Here's a jsFiddle showing the problem and contrasting it with the proposed _.paced method (which IMHO is much simpler code and accomplishes the same goal): http://jsfiddle.net/3hYPW/1/
  • @bman654:

    We differ in opinions. I specifically coded the function to make sure that the last throttled event is always triggered. I don't consider it an "extra" call, but I consider it a finalized call.

    In the vast majority of use cases I can think of, I want to make sure that if I get X hits on a function, that I make sure to invoke the first and last calls--otherwise my code is outdated.

    The common place I might use throttle is to during mouse movements and window resizing--in both cases it's important to fire off the last execution--otherwise you end up an in incomplete state.

    The only times I can see why you'd want to discard the final execution, is in the cases where you only want executions to occur when a specific event (like a specific key being held down) is ongoing. In those cases I can see where you might want to exclude the trailing execution. That use case just rarely ever happens in my development though. I can see where that might be more common in gaming development though--such as to throttle how frequently an avatar can "shoot" or "jump".
  • Got it. You might want to highlight that your throttle implementation guarantees that the last call to the throttled method will fire, though its call may be delayed. This functionality will get you in trouble when implementing throttled drag events:

    mousedown -> send dragstart
    mousemove -> send throttled dragmove
    mousemove -> send throttled dragmove
    mousemove -> send throttled dragmove
    mouseup -> send dragend

    (some time later, last call to throttled dragmove fires--AFTER the dragend event!)

    Seems like we really need a function that can do either since I certainly see your point for things like resize events.
  • @bman654:

    That makes perfect sense. I've added a "trail" argument which you can set to false in order to skip trailing execution. This should give us the best of both worlds.
  • Your throttle implementation is exactly how I think underscore's should work; do you have a license that you've released this code under? I'd like to use it in a work project and my work is picky about that kind of thing.
  • Consider it dual licensed under:

    MIT License
    http://en.wikipedia.org/wiki/MIT_License

    GPL v2
    http://en.wikipedia.org/wiki/GNU_General_Public_Li...

    Basically the exact same license as jQuery.
  • Awesome, thanks.

Comments for this entry have been disabled.