Speed up your JavaScript, Part 2
Last week, I covered the first reason why JavaScript can take too long to execute: too much happening in a loop. There’s a similar problem with functions in that sometimes they’re just doing too much. Usually this means there’s too many loops (as opposed to too much happening in a loop), too much recursion, or simply too many different operations being performed.
Too many loops are often caused by having loops inside of loops, locking up the JavaScript engine until all iterations are complete. The most glaring example of this is the bubble sort algorithm. Though there’s no need to use this in JavaScript due to the native sort() method, it’s good to understand how it can be problematic so that you can identify similar patterns. A typical implementation of a bubble sort in JavaScript looks like this:
function bubbleSort(items){
for (var i=items.length-1; i >= 0; i--){
for (var j=items.length-i; j >= 0; j--){
if (items[j] < items[j-1]){
var temp = items[j];
items[j] = items[j-1];
items[j-1] = temp;
}
}
}
}
Thinking back to your computer science days, you’ll probably remember that bubble sort is one of the least efficient sorting algorithms. The problem is for every n items in the array, there must be n2 loop iterations. This processing can take forever if there’s a large amount of array items. The comparison and swap operation done during the inner loop is actually quite simple, it’s just the number of times that it’s repeated in sequence that causes the problem. This can cause the browser to grind to a halt and, potentially, result in the long-running script dialog.
A couple years ago, fellow Yahoo Julien Lecomte wrote a post entitled,
Running CPU Intensive JavaScript Computations in a Web Browser, in which he described how to break up large JavaScript operations into several parts. One of his clearest examples was refactoring a bubble sort into multiple steps, each of which executes a single trip through the array. I’ve augmented his code somewhat, but the approach remains the same:
function bubbleSort(array, onComplete){
var pos = 0;
(function(){
var j, value;
for (j=array.length; j > pos; j--){
if (array[j] < array[j-1]){
value = data[j];
data[j] = data[j-1];
data[j-1] = value;
}
}
pos++;
if (pos < array.length){
setTimeout(arguments.callee,10);
} else {
onComplete();
}
})();
}
This function performs a bubble sort in an asynchronous manner, stopping after each trip through the array before continuing on to the next leg. The onComplete() function is called when the array is completely sorted as notification that the data is ready. The bubbleSort() function uses the same basic technique as the chunk() function presented in my last post: use an anonymous function to wrap the behavior and then pass arguments.callee into setTimeout() to repeat the process until complete. This function is a good example of how you can split up embedded loops into a series of steps to free up the browser.
A similar problem is too much recursion. Each additional recursive call takes up memory, and eventually will slow down the browser. The annoying thing is that you may reach a memory limit before the long-running script dialog pops up and leave the browser in an unusable state. Crockford had a good discussion about this in his latest talk. The example he uses is a function that generates a Fibonacci sequence:
function fibonacci (n) {
return n < 2 ? n :
fibonacci(n - 1) +
fibonacci(n - 2);
};
As Crockford points out, a call to fibonacci(40) results in 331,160,280 calls to itself. The solution to avoid too much recursion is to use memoization, a technique for caching previously-calculated values. Crockford introduces the following memoization function that can be used to create memoized versions of functions dealing with numbers:
function memoizer(memo, fundamental) {
var shell = function (n) {
var result = memo[n];
if (typeof result !== 'number') {
result = fundamental(shell, n);
memo[n] = result;
}
return result;
};
return shell;
};
He then applies this to the Fibonacci sequence generator:
var fibonacci =
memoizer([0, 1], function (recur, n) {
return recur(n - 1) + recur(n - 2);
});
Calling fibonacci(40) using this code results in only 40 calls to the function, a vast improvement over the original. The overall lesson from memoization is that you should never calculate the same result twice; if there’s a value you’ll need more than once, store it for later use rather than running the code to generate it again.
The final thing that causes functions to execute slowly is, as mentioned previously, that it’s just doing too much. Usually it’s because of a pattern such as this:
function doAlot(){
doSomething();
doSomethingElse();
doOneMoreThing();
}
Here, there’s three clearly distinct pieces of code that are being executed. The important thing to notice is that none of the functions rely on the other functions to complete their task; they are essentially independent of one another and just need to happen in sequence at a given point in time. In situations like this, you can use a variant of the chunk() method to execute a series of functions in a row without holding up the browser:
function schedule(functions, context){
setTimeout(function(){
var process = functions.shift();
process.call(context);
if (functions.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}
The schedule function accepts two arguments, an array of functions to execute and a context object indicating the value of this inside of each function. The functions array acts as a queue, with the topmost function being removed and executed each time the timer is executed. This function can be used to execute a series of functions in a row like this:
schedule([doSomething, doSomethingElse, doOneMoreThing], window);
I’m expecting that JavaScript libraries will soon start including more processing functions such as this. YUI has already added the Queue object in version 3.0 that helps to manage the running of several functions in a row using a timer.
Regardless of the tools available to help split up complex processes, it’s still vital for developers to be able to understand and identify bottlenecks that will benefit from using this approach. Whether there be too many loops, too much recursion, or just plain too much going on, you now know how to deal with each. Remember, the techniques and functions presented here are just a starting point and not a golden bullet, you should (and will likely have to) modify the code presented so that it works for your specific usage.
Update (1/20): Fixed copy/paste error in schedule() function.
Translations
Disclaimer: Any viewpoints and opinions expressed in this article are those of Nicholas C. Zakas and do not, in any way, reflect those of Yahoo!, Wrox Publishing, O'Reilly Publishing, or anyone else. I speak only for myself, not for them.
You can leave a response, or trackback from your own site.




25 Comments
Hey Nicholas,
You’re code has a flaw. You are using the call method on “process” instead of “item”:
function schedule(functions, context){
setTimeout(function(){
var item = functions.shift();
// Not process, but item…
process.call(context);
if (functions.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}
Change that and you’re good to go!
Cheers.
Joe McCann
Joe McCann on January 20th, 2009 at 5:51 pm
Hey Joe, thanks for the heads up! I’ve fixed the code.
Nicholas C. Zakas on January 20th, 2009 at 7:15 pm
No worries! Read your first book and am looking forward to this edition as well.
Cheers.
Joe
Joseph McCann on January 22nd, 2009 at 12:15 am
[...] C. Zakas is back at it with part two of his Speed up your JavaScript [...]
Ajaxian » Speed up your JavaScript with memoization and queueing on January 23rd, 2009 at 7:04 am
Thanks for the information
Timothy on January 23rd, 2009 at 12:29 pm
Speed up your JavaScript, Part 2…
Thank you for submitting this cool story - Trackback from DotNetShoutout…
DotNetShoutout on January 24th, 2009 at 12:38 am
Good post. My data structures professor was one of the biggest pricks I’ve ever met but he did say something that I never forgot: “If your program is too slow then its time to pick a better algorithm!” So I would say that it would make more sense to replace a bubble sort with a merge sort if a large amount of data needs to be sorted in a JavaScript or any other program.
Topnotch on January 24th, 2009 at 7:56 am
Hi Nicholas
I like your article - very informative. I tried creating a more generic method for memoization. Maybe you can make some use of it - see http://www.jslab.dk/library/Function.memoize for more info.
Dok on January 24th, 2009 at 11:28 am
[...] Speed up your JavaScript, Part 2 [...]
Interesting Finds: 2009.01.24 | Web Hosting and Domains on January 24th, 2009 at 12:42 pm
[...] Nicholas C. Zakas снова ускоряет JavaScript [...]
Linkdump #3 | CTAPbIu_MABP's BLOG on January 25th, 2009 at 2:50 pm
[...] halt or quit unexpectedly, and so must be addressed a serious performance problem in JavaScript. In part 2 of this series, I wrote briefly about handling too much recursion in a function through [...]
Speed up your JavaScript, Part 3 | NCZOnline on January 27th, 2009 at 9:01 am
[...] Speed up your JavaScript, Part 2 NCZOnline (tags: javascript) [...]
links for 2009-01-29 « pabloidz on January 29th, 2009 at 8:01 am
YUI 3 Queue util is based on work done for YUI 2 DataTable in which it is also a standalone util named Chain.
http://developer.yahoo.com/yui/docs/YAHOO.util.Chain.html
Unlike Queue, it is not offered a la carte, though the sources are available at http://github.com/yui.
Luke on January 29th, 2009 at 12:32 pm
[...] for speeding up your JavaScript. Part 1 covered how to deal with loops that are doing too much. Part 2 focused on functions that do too much and taught techniques such as queuing and memoization to [...]
Speed up your JavaScript, Part 4 | NCZOnline on February 3rd, 2009 at 1:20 pm
[...] Speed up your JavaScript, Part 2 | NCZOnline. [...]
My Bad Attitude » Speed up your JavaScript on February 5th, 2009 at 12:25 pm
[...] 【原文标题】Speed up your JavaScript, Part 2【原文作者】Nicholas C. [...]
如何提升JavaScript的运行速度(函数篇) « 七月佑安 on March 15th, 2009 at 7:08 am
[...] C. Zakas presents, among other things, an interesting way of scheduling work to be done in batches, so the browser doesn’t become unresponsive for the [...]
queuing for batch scheduling - Jason's postings and stuff on May 3rd, 2009 at 7:08 pm
[...] 【原文标题】Speed up your JavaScript, Part 2 【原文作者】Nicholas C. [...]
如何提升JavaScript的运行速度(函数篇) « iUE on June 3rd, 2009 at 11:33 am
[...] Speed up your Javascript part 2 [...]
Speed up your Javascript - Code Couch on June 6th, 2009 at 12:52 pm
[...] come and share some tips along those lines to the folks at Google. In following along my series of blog posts about JavaScript performance, I entitled this talk, Speed up your [...]
Speed up your JavaScript: The talk | NCZOnline on June 8th, 2009 at 11:12 pm
[...] 在过去的几周中,我为大家介绍了几种可以加快JavaScript脚本运行速度的技术。第一节介绍了如何优化循环。第二节的重点放在优化函数内部代码上,还介绍了队列(queuing)和记忆化(memoization)两种技术,来减轻函数的工作负担。第三节就如何将递归转换为迭代循环或者记忆化方式的话题,展开了讨论。第四节是这个系列的最后一篇,也就是本文,将重点阐述过多的DOM操作所带来的影响。 [...]
如何提升JavaScript的运行速度(DOM篇) « Oragg.com on June 14th, 2009 at 11:16 am
[...] 本文译自Nicholas C. Zakas于2009年1月20日在个人网站上发表的《Speed up your JavaScript, Part 2》。原文是唯一的正式版,本文是经过原文作者授权的简体中文翻译版。译者在翻译的准确性上做了大量的努力,并承诺译文的内容完全忠于原文,但可能还是包含疏漏和不妥之处,欢迎大家指正。译序和译注的内容是非正式的,仅代表译者个人观点。 [...]
七月佑安 on June 14th, 2009 at 1:39 pm
[...] 递归是拖慢脚本运行速度的大敌之一。太多的递归会让浏览器变得越来越慢直到死掉或者莫名其妙的突然自动退出,所以我们一定要解决在JavaScript中出现的这一系列性能问题。在这个系列文章的第二篇中,我曾经简短的介绍了如何通过memoization技术来替代函数中太多的递归调用。memoization是一种可以缓存之前运算结果的技术,这样我们就不需要重新计算那些已经计算过的结果。对于通过递归来进行计算的函数,memoization简直是太有用了。我现在使用的memoizer是由 Crockford写的,主要应用在那些返回整数的递归运算中。当然并不是所有的递归函数都返回整数,所以我们需要一个更加通用的memoizer()函数来处理更多类型的递归函数。 [...]
如何提升JavaScript的运行速度(递归篇) « 七月佑安 on June 14th, 2009 at 2:29 pm
[...] lot of research on performance, resulting in the Speed Up Your JavaScript blog post series (part 1, part 2, part 3, part 4) as well as several talks, namely JavaScript Variable Performance at the San [...]
Announcing High Performance JavaScript | NCZOnline on February 9th, 2010 at 9:02 am
Whoa, thank you very much this is an awesome article ( yes I know it was submitted over a year ago ). Thanks, I’m going to try out these ( part 1 as well ) on my own code to hopefully improve it
James on February 26th, 2010 at 2:22 am
Leave a Comment