Rate Limiting with JavaScript
by Peter Higgins

Sometimes you need to be alerted when some event or action happens, but the event or action could happen multiple times in quick succession. A perfect example of this is window.onscroll. The window.onscroll event fires entirely too much. Not only that, it fires inconsistently across browsers. This has been talked about before. Here, I offer a solution:

We can make perfect use of some JavaScript built-ins: setTimeout and clearTimeout, and some uber-cool Dojo magic: dojo.publish and dojo.subscribe. PubSub is a mechanism for arbitrary communication between elements. The usage pattern fits here perfectly. Because window.onscroll fires so much, having multiple connections to this event can cause serious slowdowns in your application when the user scrolls. This technique involves connecting to window.onscroll once, and rate-limiting the firing of this event to something more manageable.

Start by making the connection. We’ll wrap it in an anonymous-self-executing function to scope our variables and keep them out of the global space:

(function(d){
	var timer, // create a variable to store a timeout 
		rate = 50 //ms .. and a variable to use for the delay
	;
 
	// setup one connection
	d.connect(d.global, "onscroll", function(e){
		// if this function has been previously called and not fired,
		// clear the timeout
		timer && clearTimeout(timer); 
		timer = setTimeout(function(){
			// publish a custom topic when this timeout executes
			d.publish("/window/scrolled", [e]);
			timer = null;
		}, rate);
	});
 
})(dojo);

If the onscroll event fires 100 times in a short period, only the last occurrence will fire the publish. We can utilize this rate-limited version of onscroll simply by subscribing to the “/window/scrolled” topic.

	dojo.subscribe("/window/scrolled", function(e){
		// do some recalculation based on knowing the scrolling is "done". 
		// will fire at MOST once per 50ms, so won't be very expensive
	});

This technique can be applied to most anything. Perhaps you have a button which sends an Ajax request. Some users double and triple-click buttons out of habit, inadequate visual feedback or a number of other reasons. We can ensure that only one click is allowed within a rate limited window. Same basic setup, different event. The original connecting code might look something like this:

(function(d){
 
	// this sends our POST based on whatever form
	var someFunction = function(e){
		dojo.xhrPost({ form:"someFormId" });
	}
 
	// setup the click events. 
	d.query(".buttons").onclick(someFunction);
 
})(dojo);

If a user clicks something with class=”buttons” rapidly in succession, `someFunction` will be called that many times, causing
many Ajax requests to be sent. Converting the code to something which will only fire once per `rate` ms is

(function(d){
 
	var someFunction = function(e){
		dojo.xhrPost({ form:"someFormId" });
	}
 
	var timer, rate = 50;
	dojo.query(".buttons").onclick(function(e){
		timer && clearTimeout(timer);
		timer = setTimeout(function(){
			someFunction({ target: e.target });
			timer = null;
		}, rate);
	});
 
})(dojo);

Getting a little more advanced, we can take this concept and reduce it to a common function. We’ll invent a new API to handle all the rate-limiting locally. We’ll just make a function which accepts an extra parameter for the rate to set:

(function(d, nl){
 
	d.connectLimited = function(rate, target, event, scope, cb, fixDom){
		// summary: Just like `dojo.connect`, but takes an additional argument BEFORE
		//		standard connect() arguments: rate. This value is used to prevent 
		//		rapid successive calls to this event
 
		var timer,
			fn = scope && cb ? d.hitch(scope, cb) : scope
		;
 
		return d.connect(target, event, function(e){
			timer && clearTimeout(timer);
			var args = arguments;
			timer = setTimeout(function(){
				fn.apply(d, args);
				timer = null;
			}, rate);
		}, null, fixDom);
 
	}
 
	nl.prototype.connectLimited = function(rate, event, scope, cb, fixDom){
		// not a straight forEach, need to shift `node` into each call
		return this.forEach(function(node){
			d.connectLimited.call(d, rate, node, event, scope, cb, fixDom);
		});
	}
 
})(dojo, dojo.NodeList);

By returning the handle from the rate-limited dojo.connect call, we are able to disconnect this event with dojo.disconnect.

Now, to use it we simply call the function passing a rate in addition to whatever we normally would have called:

	dojo.query(".buttons").connectLimited(50, "onclick", someFunction);
	// or
	dojo.connectLimited(75, window, "onscroll", function(){
		dojo.publish("/window/scrolled");
	});

Hope this helps someone.

Tags: , ,

This entry was posted on Monday, September 28th, 2009 at 5:36 pm and is filed under Dojo Cookies. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

3 Responses to “Rate Limiting with JavaScript”

  1. Les Says:

    I wonder if the dojo.connectPublisher function could be modified to accept additional ‘rate’ argument, e.g.

    dojo.connectPublisher(”/window/scrolled”, window, “onscroll”, 75);

    …or a new function could be created, e.g.:

    dojo.connectLimitedPublisher(”/window/scrolled”, window, “onscroll”, 75);

  2. dante Says:

    Simon Willison created the opposite of this technique briefly:

    http://gist.github.com/292562

    While this will trap events and fire the LAST event triggered after a timeout, Simon’s will fire the FIRST event and not again until after a timeout threshold. Both have great applications.

  3. CSS Technique: Morning Sunset « Stoat - Where? Says:

    [...] on to prevent the event firing continuously and slowing down the page. That came from a helpful Dojo Cookie by Peter Higgins over at Dojo Campus. I haven’t played fully with the rate limiting yet, but [...]

Leave a Reply

You must be logged in to post a comment.