Function Debouncing with Underscore.js

By  on  

The ability to listen and react to user interactions with JavaScript is fundamental and incredibly useful. Some interactions happen frequently and some rarely. Some listener functions are light of action, others can be quite taxing on the browser. Take window's resize event for example: the event fires at each step in the resize, so if you have a taxing event listener, your user's browser will get bogged down quickly.

Obviously we can't allow the user's browser to get bogged down, but we can't simply remove listener function either. What we can do, however, is use debouncing to temper the amount of time the method runs. Instead of the listener function firing on each iteration of the resize event, we can ensure it fires only every n milliseconds during the resize, allowing our functionality to fire but at a rate so as to not ruin the user's experience. A great utility called Underscore.js provides an easy to use method for easily creating debouncing event listener functions.

The JavaScript

Creating a debouncing event listener is as easy as:

// Create the listener function
var updateLayout = _.debounce(function(e) {

	// Does all the layout updating here
	
}, 500); // Maximum run of once per 500 milliseconds

// Add the event listener
window.addEventListener("resize", updateLayout, false);

..because the Underscore.js code underneath the hood manages the interval checks and original listener function calling:

// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
_.debounce = function(func, wait, immediate) {
	var timeout;
	return function() {
		var context = this, args = arguments;
		var later = function() {
			timeout = null;
			if (!immediate) func.apply(context, args);
		};
		var callNow = immediate && !timeout;
		clearTimeout(timeout);
		timeout = setTimeout(later, wait);
		if (callNow) func.apply(context, args);
	};
};

Not the most complex piece of code but nice that you don't have to write it yourself. The debounce method doesn't rely on any other Underscore.js methods, so you can add this method to a framework like jQuery or MooTools quite easily:

// MooTools
Function.implement({
	debounce: function(wait, immediate) {
		var timeout, 
		    func = this;
		return function() {
			var context = this, args = arguments;
			var later = function() {
				timeout = null;
				if (!immediate) func.apply(context, args);
			};
			var callNow = immediate && !timeout;
			clearTimeout(timeout);
			timeout = setTimeout(later, wait);
			if (callNow) func.apply(context, args);
		};
	}
});

// Use it!
window.addEvent("resize", myFn.debounce(500));

As mentioned above, window resize events are the most obvious place to use debouncing, but you could also use them for key events that trigger an autocompleter. I love tiny pieces of code like this that can enhance a site's efficiency so quickly! I also recommend you take a look at Underscore.js and the numerous utility functions it provides -- enrich your existing framework or use it as is!

Recent Features

  • By
    An Interview with Eric Meyer

    Your early CSS books were instrumental in pushing my love for front end technologies. What was it about CSS that you fell in love with and drove you to write about it? At first blush, it was the simplicity of it as compared to the table-and-spacer...

  • By
    LightFace:  Facebook Lightbox for MooTools

    One of the web components I've always loved has been Facebook's modal dialog.  This "lightbox" isn't like others:  no dark overlay, no obnoxious animating to size, and it doesn't try to do "too much."  With Facebook's dialog in mind, I've created LightFace:  a Facebook lightbox...

Incredible Demos

  • By
    Checkbox Filtering Using MooTools ElementFilter

    When I first wrote MooTools ElementFilter, I didn't think much of it. Fast forward eight months later and I've realized I've used the plugin a billion times. Hell, even one of the "big 3" search engines is using it for their maps application.

  • By
    HTML5’s placeholder Attribute

    HTML5 has introduced many features to the browser;  some HTML-based, some in the form of JavaScript APIs, but all of them useful.  One of my favorites if the introduction of the placeholder attribute to INPUT elements.  The placeholder attribute shows text in a field until the...

Discussion

  1. Aicke Schulz

    I want to mention, that there are already two similar solutions for MooTools, realized as “pseudo events”:

    http://mootools.net/docs/more/Class/Events.Pseudos#Pseudos:throttle
    http://mootools.net/docs/more/Class/Events.Pseudos#Pseudos:pause

  2. Rolf

    Yes yes, exactly why I ripped that method out of _.js code and put it inside my Moo code for window resize events :) – good tip to share!

  3. piotr

    In case you are already using Mootools in the project, consider Events.Pseudos :once, :throttle and :pause (http://mootools.net/docs/more/Class/Events.Pseudos)

    window.addEvent('resize:throttle(400)', function(){
        // Will only fire once every 400 ms
    });
  4. Alex

    You always post things just in time. THANKS!

  5. good one! i think it’s the same with Ben Alman’s debounce http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/ which is a jQuery plugin

  6. This is really useful. very snap and play which I like. I’m interested to see if this will work on a mobile device and what listeners other listeners I could use. Thanks for the post.

  7. Thanks, really interesting.

  8. Albert

    Great, just what I needed. Can’t use MooTools event.pseudos because I’m bounded to version 1.2.5

  9. Alfonso Perez

    This is the nth-time I benefit from one of your posts, so I have decided to take some seconds to say: thanks!,

    btw, as my humble way of saying thanks, I have paused for a moment adblock and show some genuine interest for your sidebar ad ;-).

  10. Volkan

    Why they used the code like that?

    func.apply(context, args);
    

    I can change it with func() and it executes as is.

  11. Guy

    Hi,
    I know this is an old post but it’s still coming up as the second hit for the google search ‘underscore debounce’ so I thought I’d chip in.

    The function you’re describing in the 2nd paragraph – “…we can ensure it fires only every n milliseconds during the resize..” seems to actually be _throttle.
    _debounce only calls the given function once, an amount of time only after it has stopped being continually fired.

    Cheers!

  12. Dick

    Damn confusing code, your documentation skills and variable naming needs improving. There’s simpler code to do this on the net too.

    • NotHim

      @Dick As stated in the article itself, this code is part of the Underscore.js library, not his code. If you have issues with the documentation and variable naming, bring it up to the Underscore.js developers.

  13. S

    So immediate only applies to the first click? I was assuming that it made all clicks immediate, so i was really confused why the timeout variable was being compared in callnow.

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!