ryanbridges.org
16Jan/130

Build a better Javascript timer

Posted by ryan bridges

JavaScript timers are old. I couldn't tell you the first time I used one. Or when I realized they weren't all that reliable. The subject came up at the water cooler recently and I decided to try a few techniques I had learned over the years to see if I could get a little more accuracy.  You can get a little more background on timers and why they are unreliable in this article from John Resig. The punchline is: JavaScript is asynchronous, so it can only add the specified function to the stack when the time comes. There is no guarantee that the function will execute immediately, as there could be any number of things in queue waiting to be executed ahead of it.  So, what can we do?

First, the setup.  We are going to create a timer that keeps track of the difference in milliseconds from its initial start time to its execution time.  The function will then insert that time in an LI element and append it to a given UL element.  The first example is a simple page layout.

		<style>
			body:{
				width:100%;
			}
			#container{
				position:relative;
				width:960px;
				margin:0 auto;
			}
			h1{
				text-align:center;
			}
			ul{
				position:relative;
				width:30%;
				float:left;
				margin:0 3px;
				background:#cecece;
			}
		</style>
		<div id="container">
			<h1>A better JavaScript Timer, Example 1</h1>
			<ul id="outputTimer1">
				<li>This is the output from our first timer.</li>
			</ul>
		</div>

Next up, we'll write our initial javascript timer the old-fasioned way.

		<script>
			var interval=5000; //5-second interval (in millis)
			(function(window){ //we'll use a closure so our examples don't conflict
				var list=document.getElementById('outputTimer1'), //get a handle to our UL element
					start=Date.now(); //set our start time
				setInterval(function(){
					var elapsed=Date.now()-start,//get the elapsed time as soon as the function is called
						li=document.createElement('LI'); //create our list item.
					li.innerText="elapsed time is: "+elapsed; //set the text
					list.appendChild(li); //and append it to the list
				}, interval)
			}(window));
		</script>

For most of you, this probably ran just fine.  The code showed perfect 5000ms intervals and I must be crazy.  If we add some entropy to the page, you'll begin to see what I mean. In example #3, I added a simple animation loop.  Now you should start to see some erratic behavior. When I ran the example, it took only 2 iterations to throw the timer off by 41ms!  If only we had a separate thread where the timer could run without interference from the rest of the page...

Web Workers to the rescue?

The first optimization is to rework our timer as a recursive function using setTimeout.  This allows the timer to have a little bit of intelligence. Rather that merely executing 5000ms from the current execution, compensate for the overcrowded call stack by computing the time the next interval would have fired, had it not been late.

Function.prototype.interval = function (ms) {
	var func = this;
	function interval() {
		var obj = this, args = arguments, nextInterval=now()+ms;
		func.apply(obj, args);
		setTimeout(function(){interval.apply(obj, args);},nextInterval-now());
	}
	return interval;
}

Then we can apply this concept in a webworker

function now(){return (new Date()).getTime()};
var interval=5000, //5-second interval (in millis)
	start=now(); //set our start time
(function(){
	var elapsed=now()-start;
	self.postMessage(elapsed);
}.interval(interval)())

And catch it in our main page

//new hotness
(function(window){ //we'll use a closure so our examples don't conflict
	var list=document.getElementById('outputTimer2'),//get a handle to our UL element
		worker = new Worker('worker.js');//create a web worker with a script containing our new timer code
	worker.addEventListener('message', function(event) {//listen for messages from the worker
		if(!event.data)return;//ignore that initial execution to kick off the timer for the sake of consistency
		var li=document.createElement('LI'); //create our list item.
		li.innerText="elapsed time is: "+event.data; //set the text to the data passed back from the worker
		list.appendChild(li); //and append it to the list
	}, false);
	worker.postMessage(""); // start the worker.
}(window));

We can see in the finished product that, though the message handler is still at the mercy of the congestion in the main page thread, it is able to heal itself and stay a bit closer to the goal.