Jump to content

Yielding with JavaScript Timers

0
  adfm's Photo
Posted May 17 2010 09:42 AM

If you're experiencing a little UI lag with your web app, you might consider using a Javascript timer to even things out. This excerpt from Nicholas C. Zakas' High Performance Javascript will review yielding with Javascript timers for a smoother user experience.


Despite your best efforts, there will be times when a Javascript task cannot be completed in 100 milliseconds or less because of its complexity. In these cases, it’s ideal to yield control of the UI thread so that UI updates may occur. Yielding control means stopping Javascript execution and giving the UI a chance to update itself before continuing to execute the Javascript. This is where Javascript timers come into the picture.

Timer Basics

Timers are created in Javascript using either setTimeout() or setInterval(), and both accept the same arguments: a function to execute and the amount of time to wait (in milliseconds) before executing it. The setTimeout() function creates a timer that executes just once, whereas the setInterval() function creates a timer that repeats periodically.

The way that timers interact with the UI thread is helpful for breaking up long-running scripts into shorter segments. Calling setTimeout() or setInterval() tells the Javascript engine to wait a certain amount of time and then add a Javascript task to the UI queue. For example:

function greeting(){

	alert("Hello world!");

}



setTimeout(greeting, 250);

This code inserts a Javascript task to execute the greeting() function into the UI queue after 250 milliseconds have passed. Prior to that point, all other UI updates and Javascript tasks are executed. Keep in mind that the second argument indicates when the task should be added to the UI queue, which is not necessarily the time that it will be executed; the task must wait until all other tasks already in the queue are executed, just like any other task. Consider the following:

var button = document.getElementById("my-button");

button.onclick = function(){



	oneMethod();



	setTimeout(function(){

 	document.getElementById("notice").style.color = "red";

	}, 250);

};

When the button in this example is clicked, it calls a method and then sets a timer. The code to change the notice element’s color is contained in a timer set to be queued in 250 milliseconds. That 250 milliseconds starts from the time at which setTimeout() is called, not when the overall function has finished executing. So if setTimeout() is called at a point in time n, then the Javascript task to execute the timer code is added to the UI queue at n + 250. Figure 6.3 shows this relationship when the button in this example is clicked.

Keep in mind that the timer code can never be executed until after the function in which it was created is completely executed. For example, if the previous code is changed such that the timer delay is smaller and there is another function call after the timer is created, it’s possible that the timer code will be queued before the onclick event handler has finished executing:

var button = document.getElementById("my-button");

button.onclick = function(){



	oneMethod();



	setTimeout(function(){

 	document.getElementById("notice").style.color = "red";

	}<strong class="userinput"><code>, 50[/inlinecode]</strong>);



<span class="strong"><strong>	anotherMethod();</strong></span>

};

Figure 6.3. The second argument of setTimeout() indicates when the new Javascript task should be inserted into the UI queue

Attached Image

If anotherMethod() takes longer than 50 milliseconds to execute, then the timer code is added to the queue before the onclick handler is finished. The effect is that the timer code executes almost immediately after the onclick handler has executed completely, without a noticeable delay. Figure 6.4 illustrates this situation.

In either case, creating a timer creates a pause in the UI thread as it switches from one task to the next. Consequently, timer code resets all of the relevant browser limits, including the long-running script timer. Further, the call stack is reset to zero inside of the timer code. These characteristics make timers the ideal cross-browser solution for long-running Javascript code.

Note

The setInterval() function is almost the same as setTimeout(), except that the former repeatedly adds Javascript tasks into the UI queue. The main difference is that it will not add a Javascript task into the UI queue if a task created by the same setInterval() call is already present in the UI queue.

Figure 6.4. There may be no noticeable delay in timer code execution if the function in which setTimeout() is called takes longer to execute than the timer delay

Attached Image

Timer Precision

Javascript timer delays are often imprecise, with slips of a few milliseconds in either direction. Just because you specify 250 milliseconds as the timer delay doesn’t necessarily mean the task is queued exactly 250 milliseconds after setTimeout() is called. All browsers make an attempt to be as accurate as possible, but oftentimes a slip of a few milliseconds in either direction occurs. For this reason, timers are unreliable for measuring actual time passed.

Timer resolution on Windows systems is 15 milliseconds, meaning that it will interpret a timer delay of 15 as either 0 or 15, depending on when the system time was last updated. Setting timer delays of less than 15 can cause browser locking in Internet Explorer, so the smallest recommended delay is 25 milliseconds (which will end up as either 15 or 30) to ensure a delay of at least 15 milliseconds.

This minimum timer delay also helps to avoid timer resolution issues in other browsers and on other systems. Most browsers show some variance in timer delays when dealing with 10 milliseconds or smaller.

Array Processing with Timers

One common cause of long-running scripts is loops that take too long to execute. If you’ve already tried the loop optimization techniques presented in Chapter 4, Algorithms and Flow Control but haven’t been able to reduce the execution time enough, then timers are your next optimization step. The basic approach is to split up the loop’s work into a series of timers.

Typical loops follow a simple pattern, such as:

for (var i=0, len=items.length; i < len; i++){

	process(items[i]);

}

Loops with this structure can take too long to execute due to the complexity of process(), the size of items, or both. In my book Professional Javascript for Web Developers, Second Edition (Wrox 2009), I lay out the two determining factors for whether a loop can be done asynchronously using timers:

  • Does the processing have to be done synchronously?

  • Does the data have to be processed sequentially?

If the answer to both of these questions is “no,” then the code is a good candidate for using timers to split up the work. A basic pattern for asynchronous code execution is:

var todo = items.concat(); //create a clone of the original



setTimeout(function(){



	//get next item in the array and process it

	process(todo.shift());

	

	//if there's more items to process, create another timer

	if(todo.length > 0){

 	setTimeout(arguments.callee, 25);

	} else {

 	callback(items);

	}



}, 25);

The basic idea of this pattern is to create a clone of the original array and use that as a queue of items to process. The first call to setTimeout() creates a timer to process the first item in the array. Calling todo.shift() returns the first item and also removes it from the array. This value is passed into process(). After processing the item, a check is made to determine whether there are more items to process. If there are still items in the todo array, there are more items to process and another timer is created. Because the next timer needs to run the same code as the original, arguments.callee is passed in as the first argument. This value points to the anonymous function in which the code is executing. If there are no further items to process, then a callback() function is called.

Note

The actual amount of time to delay each timer is largely dependent on your use case. Generally speaking, it’s best to use at least 25 milliseconds because smaller delays leave too little time for most UI updates.

Because this pattern requires significantly more code that a regular loop, it’s useful to encapsulate this functionality. For example:

function processArray(items, process, callback){

	var todo = items.concat(); //create a clone of the original



	setTimeout(function(){

 	process(todo.shift());



 	if (todo.length > 0){

 	setTimeout(arguments.callee, 25);

 	} else {

 	callback(items);

 	}



	}, 25);	

}

The processArray() function implements the previous pattern in a reusable way and accepts three arguments: the array to process, the function to call on each item, and a callback function to execute when processing is complete. This function can be used as follows:

var items = [123, 789, 323, 778, 232, 654, 219, 543, 321, 160];



function outputValue(value){

	console.log(value);

}



processArray(items, outputValue, function(){

	console.log("Done!");

});

This code uses the processArray() method to output array values to the console and then prints a message when all processing is complete. By encapsulating the timer code inside of a function, it can be reused in multiple places without requiring multiple implementations.

Note

One side effect of using timers to process arrays is that the total time to process the array increases. This is because the UI thread is freed up after each item is processed and there is a delay before the next item is processed. Nevertheless, this is a necessary trade-off to avoid a poor user experience by locking up the browser.

Splitting Up Tasks

What we typically think of as one task can often be broken down into a series of subtasks. If a single function is taking too long to execute, check to see whether it can be broken down into a series of smaller functions that complete in smaller amounts of time. This is often as simple as considering a single line of code as an atomic task, even though multiple lines of code typically can be grouped together into a single task. Some functions are already easily broken down based on the other functions they call. For example:

function saveDocument(id){



	//save the document

	openDocument(id)

	writeText(id);

	closeDocument(id);



	//update the UI to indicate success

	updateUI(id);

}

If this function is taking too long, it can easily be split up into a series of smaller steps by breaking out the individual methods into separate timers. You can accomplish this by adding each function into an array and then using a pattern similar to the array-processing pattern from the previous section:

function saveDocument(id){



	var tasks = [openDocument, writeText, closeDocument, updateUI];



	setTimeout(function(){

	

 	//execute the next task

 	var task = tasks.shift();

 	task(id);

 	

 	//determine if there's more

 	if (tasks.length > 0){

 	setTimeout(arguments.callee, 25);

 	}

	}, 25);

}

This version of the function places each method into the tasks array and then executes only one method with each timer. Fundamentally, this now becomes an array-processing pattern, with the sole difference that processing an item involves executing the function contained in the item. As discussed in the previous section, this pattern can be encapsulated for reuse:

function multistep(steps, args, callback){



	var tasks = steps.concat(); //clone the array



	setTimeout(function(){

	

 	//execute the next task

 	var task = tasks.shift();

 	task.apply(null, args || []);

 	

 	//determine if there's more

 	if (tasks.length > 0){

 	setTimeout(arguments.callee, 25);

 	} else {

 	callback();

 	}

	}, 25);

}

The multistep() function accepts three arguments: an array of functions to execute, an array of arguments to pass into each function when it executes, and a callback function to call when the process is complete. This function can be used like the following:

function saveDocument(id){



	var tasks = [openDocument, writeText, closeDocument, updateUI];

	multistep(tasks, [id], function(){

 	alert("Save completed!");

	});

}

Note that the second argument to multistep() must be an array, so one is created containing just id. As with array processing, this function is best used when the tasks can be processed asynchronously without affecting the user experience or causing errors in dependent code.

Timed Code

Sometimes executing just one task at a time is inefficient. Consider processing an array of 1,000 items for which processing a single item takes 1 millisecond. If one item is processed in each timer and there is a delay of 25 milliseconds in between, that means the total amount of time to process the array is (25 + 1) × 1,000 = 26,000 milliseconds, or 26 seconds. What if you processed the items in batches of 50 with a 25-millisecond delay between them? The entire processing time then becomes (1,000 / 50) × 25 + 1,000 = 1,500 milliseconds, or 1.5 seconds, and the user is still never blocked from the interface because the longest the script has executed continuously is 50 milliseconds. It’s typically faster to process items in batches than one at a time.

If you keep 100 milliseconds in mind as the absolute maximum amount of time that Javascript should be allowed to run continuously, then you can start optimizing the previous patterns. My recommendation is to cut that number in half and never let any Javascript code execute for longer than 50 milliseconds continuously, just to make sure the code never gets close to affecting the user experience.

It’s possible to track how long a piece of code has been running by using the native Date object. This is the way most Javascript profiling works:

var start = +new Date(),

	stop;



someLongProcess();



stop = +new Date();



if(stop-start < 50){

	alert("Just about right.");

} else {

	alert("Taking too long.");

}

Since each new Date object is initialized with the current system time, you can time code by creating new Date objects periodically and comparing their values. The plus operator (+) converts the Date object into a numeric representation so that any further arithmetic doesn’t involve conversions. This same basic technique can be used to optimize the previous timer patterns.

The processArray() method can be augmented to process multiple items per timer by adding in a time check:

function timedProcessArray(items, process, callback){

	var todo = items.concat(); //create a clone of the original



	setTimeout(function(){

 	var start = +new Date();



<span class="strong"><strong> 	do {</strong></span>

<span class="strong"><strong> 	process(todo.shift());</strong></span>

<span class="strong"><strong> 	} while (todo.length > 0 &amp;&amp; (+new Date() - start < 50));</strong></span>



 	if (todo.length > 0){

 	setTimeout(arguments.callee, 25);

 	} else {

 	callback(items);

 	}



	}, 25);

}

The addition of a do-while loop in this function enables checking the time after each item is processed. The array will always contain at least one item when the timer function executes, so a post-test loop makes more sense than a pretest one. When run in Firefox 3, this function processes an array of 1,000 items, where process() is an empty function, in 38–43 milliseconds; the original processArray() function processes the same array in over 25,000 milliseconds. This is the power of timing tasks before breaking them up into smaller chunks.

Timers and Performance

Timers can make a huge difference in the overall performance of your Javascript code, but overusing them can have a negative effect on performance. The code in this section has used sequenced timers such that only one timer exists at a time and new ones are created only when the last timer has finished. Using timers in this way will not result in performance issues.

Performance issues start to appear when multiple repeating timers are being created at the same time. Since there is only one UI thread, all of the timers compete for time to execute. Neil Thomas of Google Mobile researched this topic as a way of measuring performance on the mobile Gmail application for the iPhone and Android.[12]

Thomas found that low-frequency repeating timers—those occurring at intervals of one second or greater—had little effect on overall web application responsiveness. The timer delays in this case are too large to create a bottleneck on the UI thread and are therefore safe to use repeatedly. When multiple repeating timers are used with a much greater frequency (between 100 and 200 milliseconds), however, Thomas found that the mobile Gmail application became noticeably slower and less responsive.

The takeaway from Thomas’s research is to limit the number of high-frequency repeating timers in your web application. Instead, Thomas suggests creating a single repeating timer that performs multiple operations with each execution.

[12] The full post is available online at http://googlecode.bl...ries-using.html.

High Performance Javascript

Learn more about this topic from High Performance Javascript.

If you're like most developers, you rely heavily on Javascript to build interactive and quick-responding web applications. The problem is that all of those lines of Javascript code can slow down your apps. This book reveals techniques and strategies to help you eliminate performance bottlenecks during development. You'll learn optimal ways to load code onto a page, programming tips to help your Javascript run as efficiently and quickly as possible, best practices to build and deploy your files to a production environment, and more.

See what you'll learn


Tags:
0 Subscribe


0 Replies