Chapter 4. Algorithms and Flow Control

The overall structure of your code is one of the main determinants as to how fast it will execute. Having a very small amount of code doesn’t necessarily mean that it will run quickly, and having a large amount of code doesn’t necessarily mean that it will run slowly. A lot of the performance impact is directly related to how the code has been organized and how you’re attempting to solve a given problem.

The techniques in this chapter aren’t necessarily unique to JavaScript and are often taught as performance optimizations for other languages. There are some deviations from advice given for other languages, though, as there are many more JavaScript engines to deal with and their quirks need to be considered, but all of the techniques are based on prevailing computer science knowledge.

Loops

In most programming languages, the majority of code execution time is spent within loops. Looping over a series of values is one of the most frequently used patterns in programming and as such is also one of the areas where efforts to improve performance must be focused. Understanding the performance impact of loops in JavaScript is especially important, as infinite or long-running loops severely impact the overall user experience.

Types of Loops

ECMA-262, 3rd Edition, the specification that defines JavaScript’s basic syntax and behavior, defines four types of loops. The first is the standard for loop, which shares its syntax with other C-like languages:

for (var i=0; i < 10; i++){
    //loop body
}

The for loop tends to be the most commonly used JavaScript looping construct. There are four parts to the for loop: initialization, pretest condition, post-execute, and the loop body. When a for loop is encountered, the initialization code is executed first, followed by the pretest condition. If the pretest condition evaluates to true, then the body of the loop is executed. After the body is executed, the post-execute code is run. The perceived encapsulation of the for loop makes it a favorite of developers.

Note

Note that placing a var statement in the initialization part of a for loop creates a function-level variable, not a loop-level one. JavaScript has only function-level scope, and so defining a new variable inside of a for loop is the same as defining a new variable outside of the loop.

The second type of loop is the while loop. A while loop is a simple pretest loop comprised of a pretest condition and a loop body:

var i = 0;
while(i < 10){
    //loop body
    i++;
}

Before the loop body is executed, the pretest condition is evaluated. If the condition evaluates to true, then the loop body is executed; otherwise, the loop body is skipped. Any for loop can also be written as a while loop and vice versa.

The third type of loop is the do-while loop. A do-while loop is the only post-test loop available in JavaScript and is made up of two parts, the loop body and the post-test condition:

var i = 0;
do {
    //loop body
} while (i++ < 10);

In a do-while loop, the loop body is always executed at least once, and the post-test condition determines whether the loop should be executed again.

The fourth and last loop is the for-in loop. This loop has a very special purpose: it enumerates the named properties of any object. The basic format is as follows:

for (var prop in object){
    //loop body
}

Each time the loop is executed, the prop variable is filled with the name of another property (a string) that exists on the object until all properties have been returned. The returned properties are both those that exist on the object instance and those inherited through its prototype chain.

Loop Performance

A constant source of debate regarding loop performance is which loop to use. Of the four loop types provided by JavaScript, only one of them is significantly slower than the others: the for-in loop.

Since each iteration through the loop results in a property lookup either on the instance or on a prototype, the for-in loop has considerably more overhead per iteration and is therefore slower than the other loops. For the same number of loop iterations, a for-in loop can end up as much as seven times slower than the other loop types. For this reason, it’s recommended to avoid the for-in loop unless your intent is to iterate over an unknown number of object properties. If you have a finite, known list of properties to iterate over, it is faster to use one of the other loop types and use a pattern such as this:

var props = ["prop1", "prop2"],
    i = 0;

while (i < props.length){
    process(object[props[i++]]);
}

This code creates an array whose members are property names. The while loop is used to iterate over this small number of properties and process the appropriate member on object. Rather than looking up each and every property on object, the code focuses on only the properties of interest, saving loop overhead and time.

Note

You should never use for-in to iterate over members of an array.

Aside from the for-in loop, all other loop types have equivalent performance characteristics such that it’s not useful to try to determine which is fastest. The choice of loop type should be based on your requirements rather than performance concerns.

If loop type doesn’t contribute to loop performance, then what does? There are actually just two factors:

  • Work done per iteration

  • Number of iterations

By decreasing either or both of these, you can positively impact the overall performance of the loop.

Decreasing the work per iteration

It stands to reason that if a single pass through a loop takes a long time to execute, then multiple passes through the loop will take even longer. Limiting the number of expensive operations done in the loop body is a good way to speed up the entire loop.

A typical array-processing loop can be created using any of the three faster loop types. The code is most frequently written as follows:

//original loops
for (var i=0; i < items.length; i++){
    process(items[i]);
}

var j=0;
while (j < items.length){
    process(items[j++]);
}

var k=0;
do {
    process(items[k++]);
} while (k < items.length);

In each of these loops, there are several operations happening each time the loop body is executed:

  1. One property lookup (items.length) in the control condition

  2. One comparison (i < items.length) in the control condition

  3. One comparison to see whether the control condition evaluates to true (i < items.length == true)

  4. One increment operation (i++)

  5. One array lookup (items[i])

  6. One function call (process(items[i]))

There’s a lot going on per iteration of these simple loops, even though there’s not much code. The speed at which the code will execute is largely determined by what process() does to each item, but even so, reducing the total number of operations per iteration can greatly improve the overall loop performance.

The first step in optimizing the amount of work in a loop is to minimize the number of object member and array item lookups. As discussed in Chapter 2, these take significantly longer to access in most browsers versus local variables or literal values. The previous examples do a property lookup for items.length each and every time through the loop. Doing so is wasteful, as this value won’t change during the execution of the loop and is therefore an unnecessary performance hit. You can improve the loop performance easily by doing the property lookup once, storing the value in a local variable, and then using that variable in the control condition:

//minimizing property lookups
for (var i=0, len=items.length; i < len; i++){
    process(items[i]);
}

var j=0,
    count = items.length;
while (j < count){
    process(items[j++]);
}

var k=0,
    num = items.length;
do {
    process(items[k++]);
} while (k < num);

Each of these rewritten loops makes a single property lookup for the array length prior to the loop executing. This allows the control condition to be comprised solely of local variables and therefore run much faster. Depending on the length of the array, you can save around 25% off the total loop execution time in most browsers (and up to 50% in Internet Explorer).

You can also increase the performance of loops by reversing their order. Frequently, the order in which array items are processed is irrelevant to the task, and so starting at the last item and processing toward the first item is an acceptable alternative. Reversing loop order is a common performance optimization in programming languages but generally isn’t very well understood. In JavaScript, reversing a loop does result in a small performance improvement for loops, provided that you eliminate extra operations as a result:

//minimizing property lookups and reversing
for (var i=items.length; i--; ){
    process(items[i]);
}

var j = items.length;
while (j--){
    process(items[j]);
}

var k = items.length-1;
do {
    process(items[k]);
} while (k--);

The loops in this example are reversed and combine the control condition with the decrement operation. Each control condition is now simply a comparison against zero. Control conditions are compared against the value true, and any nonzero number is automatically coerced to true, making zero the equivalent of false. Effectively, the control condition has been changed from two comparisons (is the iterator less than the total and is that equal to true?) to just a single comparison (is the value true?). Cutting down from two comparisons per iteration to one speeds up the loops even further. By reversing loops and minimizing property lookups, you can see execution times that are up to 50%–60% faster than the original.

As a comparison to the originals, here are the operations being performed per iteration for these loops:

  1. One comparison (i == true) in the control condition

  2. One decrement operation (i--)

  3. One array lookup (items[i])

  4. One function call (process(items[i]))

The new loop code has two fewer operations per iteration, which can lead to increasing performance gains as the number of iterations increases.

Note

Decreasing the work done per iteration is most effective when the loop has a complexity of O(n). When the loop is more complex than O(n), it is advisable to focus your attention on decreasing the number of iterations.

Decreasing the number of iterations

Even the fastest code in a loop body will add up when iterated thousands of times. Additionally, there is a small amount of performance overhead associated with executing a loop body, which just adds to the overall execution time. Decreasing the number of iterations throughout the loop can therefore lead to greater performance gains. The most well known approach to limiting loop iterations is a pattern called Duff’s Device.

Duff’s Device is a technique of unrolling loop bodies so that each iteration actually does the job of many iterations. Jeff Greenberg is credited with the first published port of Duff’s Device to JavaScript from its original implementation in C. A typical implementation looks like this:

//credit: Jeff Greenberg
var iterations = Math.floor(items.length / 8),
    startAt    = items.length % 8,
    i          = 0;

do {
    switch(startAt){
        case 0: process(items[i++]);
        case 7: process(items[i++]);
        case 6: process(items[i++]);
        case 5: process(items[i++]);
        case 4: process(items[i++]);
        case 3: process(items[i++]);
        case 2: process(items[i++]);
        case 1: process(items[i++]);
    }
    startAt = 0;
} while (iterations--);

The basic idea behind this Duff’s Device implementation is that each trip through the loop is allowed a maximum of eight calls to process(). The number of iterations through the loop is determined by dividing the total number of items by eight. Because not all numbers are evenly divisible by eight, the startAt variable holds the remainder and indicates how many calls to process() will occur in the first trip through the loop. If there were 12 items, then the first trip through the loop would call process() 4 times, and then the second trip would call process() 8 times, for a total of two trips through the loop instead of 12.

A slightly faster version of this algorithm removes the switch statement and separates the remainder processing from the main processing:

//credit: Jeff Greenberg
var iterations = items.length % 8;
i	= items.length -1;

while(iterations){
	process(items[i--]);
	iterations--;
}

iterations = Math.floor(items.length / 8);

while(iterations){
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    iterations--;
}

Even though this implementation is now two loops instead of one, it runs faster than the original by removing the switch statement from the loop body.

Whether or not it’s worthwhile to use Duff’s Device, either the original or the modified version, depends largely on the number of iterations you’re already doing. In cases where the loop iterations are less than 1,000, you’re likely to see only an insignificant amount of performance improvement over using a regular loop construct. As the number of iterations increases past 1,000, however, the efficacy of Duff’s Device increases significantly. At 500,000 iterations, for instance, the execution time is up to 70% less than a regular loop.

Function-Based Iteration

The fifth edition of ECMA-262 introduced a new method on the native array object called forEach(). This method iterates over the members of an array and runs a function on each. The function to be run on each item is passed into forEach() as an argument and will receive three arguments when called, which are the array item value, the index of the array item, and the array itself. The following is an example usage:

items.forEach(function(value, index, array){
    process(value);
});

The forEach() method is implemented natively in Firefox, Chrome, and Safari. Additionally, most JavaScript libraries have the logical equivalent:

//YUI 3
Y.Array.each(items, function(value, index, array){
    process(value);
});

//jQuery
jQuery.each(items, function(index, value){
    process(value);
});

//Dojo
dojo.forEach(items, function(value, index, array){
    process(value);
});

//Prototype
items.each(function(value, index){
    process(value);
});

//MooTools
$each(items, function(value, index){
    process(value);
});

Even though function-based iteration represents a more convenient method of iteration, it is also quite a bit slower than loop-based iteration. The slowdown can be accounted for by the overhead associated with an extra method being called on each array item. In all cases, function-based iteration takes up to eight times as long as loop-based iteration and therefore isn’t a suitable approach when execution time is a significant concern.

Conditionals

Similar in nature to loops, conditionals determine how execution flows through JavaScript. The traditional argument of whether to use if-else statements or a switch statement applies to JavaScript just as it does to other languages. Since different browsers have implemented different flow control optimizations, it is not always clear which technique to use.

if-else Versus switch

The prevailing theory on using if-else versus switch is based on the number of conditions being tested: the larger the number of conditions, the more inclined you are to use a switch instead of if-else. This typically comes down to which code is easier to read. The argument is that if-else is easier to read when there are fewer conditions and switch is easier to read when the number of conditions is large. Consider the following:

if (found){
    //do something
} else {
    //do something else
}

switch(found){
    case true:
        //do something
        break;

    default:
        //do something else
}

Though both pieces of code perform the same task, many would argue that the if-else statement is much easier to read than the switch. Increasing the number of conditions, however, usually reverses that opinion:

if (color == "red"){
    //do something
} else if (color == "blue"){
    //do something
} else if (color == "brown"){
    //do something
} else if (color == "black"){
    //do something
} else {
    //do something
}

switch (color){
    case "red":
        //do something
        break;

    case "blue":
        //do something
        break;

    case "brown":
        //do something
        break;

    case "black":
        //do something
        break;

    default:
        //do something
}

Most would consider the switch statement in this code to be more readable than the if-else statement.

As it turns out, the switch statement is faster in most cases when compared to if-else, but significantly faster only when the number of conditions is large. The primary difference in performance between the two is that the incremental cost of an additional condition is larger for if-else than it is for switch. Therefore, our natural inclination to use if-else for a small number of conditions and a switch statement for a larger number of conditions is exactly the right advice when considering performance.

Generally speaking, if-else is best used when there are two discrete values or a few different ranges of values for which to test. When there are more than two discrete values for which to test, the switch statement is the most optimal choice.

Optimizing if-else

When optimizing if-else, the goal is always to minimize the number of conditions to evaluate before taking the correct path. The easiest optimization is therefore to ensure that the most common conditions are first. Consider the following:

if (value < 5) {
    //do something
} else if (value > 5 && value < 10) {
    //do something
} else {
    //do something
}

This code is optimal only if value is most frequently less than 5. If value is typically greater than or equal to 10, then two conditions must be evaluated each time before the correct path is taken, ultimately increasing the average amount of time spent in this statement. Conditions in an if-else should always be ordered from most likely to least likely to ensure the fastest possible execution time.

Another approach to minimizing condition evaluations is to organize the if-else into a series of nested if-else statements. Using a single, large if-else typically leads to slower overall execution time as each additional condition is evaluated. For example:

if (value == 0){
    return result0;
} else if (value == 1){
    return result1;
} else if (value == 2){
    return result2;
} else if (value == 3){
    return result3;
} else if (value == 4){
    return result4;
} else if (value == 5){
    return result5;
} else if (value == 6){
    return result6;
} else if (value == 7){
    return result7;
} else if (value == 8){
    return result8;
} else if (value == 9){
    return result9;
} else {
    return result10;
}

With this if-else statement, the maximum number of conditions to evaluate is 10. This slows down the average execution time if you assume that the possible values for value are evenly distributed between 0 and 10. To minimize the number of conditions to evaluate, the code can be rewritten into a series of nested if-else statements, such as:

if (value < 6){

    if (value < 3){
        if (value == 0){
            return result0;
        } else if (value == 1){
            return result1;
        } else {
            return result2;
        }
    } else {
        if (value == 3){
            return result3;
        } else if (value == 4){
            return result4;
        } else {
            return result5;
        }
    }

} else {

    if (value < 8){
        if (value == 6){
            return result6;
        } else {
            return result7;
        }
    } else {
        if (value == 8){
            return result8;
        } else if (value == 9){
            return result9;
        } else {
            return result10;
        }
    }
}

The rewritten if-else statement has a maximum number of four condition evaluations each time through. This is achieved by applying a binary-search-like approach, splitting the possible values into a series of ranges to check and then drilling down further in that section. The average amount of time it takes to execute this code is roughly half of the time it takes to execute the previous if-else statement when the values are evenly distributed between 0 and 10. This approach is best when there are ranges of values for which to test (as opposed to discrete values, in which case a switch statement is typically more appropriate).

Lookup Tables

Sometimes the best approach to conditionals is to avoid using if-else and switch altogether. When there are a large number of discrete values for which to test, both if-else and switch are significantly slower than using a lookup table. Lookup tables can be created using arrays or regular objects in JavaScript, and accessing data from a lookup table is much faster than using if-else or switch, especially when the number of conditions is large (see Figure 4-1).

Array item lookup versus using if-else or switch in Internet Explorer 7
Figure 4-1. Array item lookup versus using if-else or switch in Internet Explorer 7

Lookup tables are not only very fast in comparison to if-else and switch, but they also help to make code more readable when there are a large number of discrete values for which to test. For example, switch statements start to get unwieldy when large, such as:

switch(value){
    case 0:
        return result0;
    case 1:
        return result1;
    case 2:
        return result2;
    case 3:
        return result3;
    case 4:
        return result4;
    case 5:
        return result5;
    case 6:
        return result6;
    case 7:
        return result7;
    case 8:
        return result8;
    case 9:
        return result9;
    default:
        return result10;
}

The amount of space that this switch statement occupies in code is probably not proportional to its importance. The entire structure can be replaced by using an array as a lookup table:

//define the array of results
var results = [result0, result1, result2, result3, result4, result5, result6,
               result7, result8, result9, result10]

//return the correct result
return results[value];

When using a lookup table, you have completely eliminated all condition evaluations. The operation becomes either an array item lookup or an object member lookup. This is a major advantage for lookup tables: since there are no conditions to evaluate, there is little or no additional overhead as the number of possible values increases.

Lookup tables are most useful when there is logical mapping between a single key and a single value (as in the previous example). A switch statement is more appropriate when each key requires a unique action or set of actions to take place.

Recursion

Complex algorithms are typically made easier by using recursion. In fact, there are some traditional algorithms that presume recursion as the implementation, such as a function to return factorials:

function factorial(n){
    if (n == 0){
        return 1;
    } else {
        return n * factorial(n-1);
    }
}

The problem with recursive functions is that an ill-defined or missing terminal condition can lead to long execution times that freeze the user interface. Further, recursive functions are more likely to run into browser call stack size limits.

Call Stack Limits

The amount of recursion supported by JavaScript engines varies and is directly related to the size of the JavaScript call stack. With the exception of Internet Explorer, for which the call stack is related to available system memory, all other browsers have static call stack limits. The call stack size for the most recent browser versions is relatively high compared to older browsers (Safari 2, for instance, had a call stack size of 100). Figure 4-2 shows call stack sizes over the major browsers.

JavaScript call stack size in browsers
Figure 4-2. JavaScript call stack size in browsers

When you exceed the maximum call stack size by introducing too much recursion, the browser will error out with one of the following messages:

  • Internet Explorer: “Stack overflow at line x”

  • Firefox: “Too much recursion”

  • Safari: “Maximum call stack size exceeded”

  • Opera: “Abort (control stack overflow)”

Chrome is the only browser that doesn’t display a message to the user when the call stack size has been exceeded.

Perhaps the most interesting part of stack overflow errors is that they are actual JavaScript errors in some browsers, and can therefore be trapped using a try-catch statement. The exception type varies based on the browser being used. In Firefox, it’s an InternalError; in Safari and Chrome, it’s a RangeError; and Internet Explorer throws a generic Error type. (Opera doesn’t throw an error; it just stops the JavaScript engine.) This makes it possible to handle such errors right from JavaScript:

try {
    recurse();
} catch (ex){
    alert("Too much recursion!");
}

If left unhandled, these errors bubble up as any other error would (in Firefox, it ends up in the Firebug and error consoles; in Safari/Chrome it shows up in the JavaScript console), except in Internet Explorer. IE will not only display a JavaScript error, but will also display a dialog box that looks just like an alert with the stack overflow message.

Note

Even though it is possible to trap these errors in JavaScript, it is not recommended. No script should ever be deployed that has the potential to exceed the maximum call stack size.

Recursion Patterns

When you run into a call stack size limit, your first step should be to identify any instances of recursion in the code. To that end, there are two recursive patterns to be aware of. The first is the straightforward recursive pattern represented in the factorial() function shown earlier, when a function calls itself. The general pattern is as follows:

function recurse(){
    recurse();
}

recurse();

This pattern is typically easy to identify when errors occur. A second, subtler pattern involves two functions:

function first(){
    second();
}

function second(){
    first();
}

first();

In this recursion pattern, two functions each call the other, such that an infinite loop is formed. This is the more troubling pattern and a far more difficult one to identify in large code bases.

Most call stack errors are related to one of these two recursion patterns. A frequent cause of stack overflow is an incorrect terminal condition, so the first step after identifying the pattern is to validate the terminal condition. If the terminal condition is correct, then the algorithm contains too much recursion to safely be run in the browser and should be changed to use iteration, memoization, or both.

Iteration

Any algorithm that can be implemented using recursion can also be implemented using iteration. Iterative algorithms typically consist of several different loops performing different aspects of the process, and thus introduce their own performance issues. However, using optimized loops in place of long-running recursive functions can result in performance improvements due to the lower overhead of loops versus that of executing a function.

As an example, the merge sort algorithm is most frequently implemented using recursion. A simple JavaScript implementation of merge sort is as follows:

function merge(left, right){
    var result = [];

    while (left.length > 0 && right.length > 0){
        if (left[0] < right[0]){
            result.push(left.shift());
        } else {
            result.push(right.shift());
        }
    }

    return result.concat(left).concat(right);
}

function mergeSort(items){

    if (items.length == 1) {
        return items;
    }

    var middle = Math.floor(items.length / 2),
        left    = items.slice(0, middle),
        right   = items.slice(middle);

    return merge(mergeSort(left), mergeSort(right));
}

The code for this merge sort is fairly simple and straightforward, but the mergeSort() function itself ends up getting called very frequently. Recursive functions are a very common cause of stack overflow errors in the browser.

Running into the stack overflow error doesn’t necessarily mean the entire algorithm has to change; it simply means that recursion isn’t the best implementation. The merge sort algorithm can also be implemented using iteration, such as:

//uses the same merge() function from previous example

function mergeSort(items){

    if (items.length == 1) {
        return items;
    }

    var work = [];
    for (var i=0, len=items.length; i < len; i++){
        work.push([items[i]]);
    }
    work.push([]);  //in case of odd number of items

    for (var lim=len; lim > 1; lim = Math.floor((lim+1)/2)){
        for (var j=0,k=0; k < lim; j++, k+=2){
            work[j] = merge(work[k], work[k+1]);
        }
        work[j] = [];  //in case of odd number of items
    }

    return work[0];
}

This implementation of mergeSort() does the same work as the previous one without using recursion. Although the iterative version of merge sort may be somewhat slower than the recursive option, it doesn’t have the same call stack impact as the recursive version. Switching recursive algorithms to iterative ones is just one of the options for avoiding stack overflow errors.

Memoization

Work avoidance is the best performance optimization technique. The less work your code has to do, the faster it executes. Along those lines, it also makes sense to avoid work repetition. Performing the same task multiple times is a waste of execution time. Memoization is an approach to avoid work repetition by caching previous calculations for later reuse, which makes memoization a useful technique for recursive algorithms.

When recursive functions are called multiple times during code execution, there tends to be a lot of work duplication. The factorial() function, introduced earlier in Recursion, is a great example of how work can be repeated multiple times by recursive functions. Consider the following code:

var fact6 = factorial(6);
var fact5 = factorial(5);
var fact4 = factorial(4);

This code produces three factorials and results in the factorial() function being called a total of 18 times. The worst part of this code is that all of the necessary work is completed on the first line. Since the factorial of 6 is equal to 6 multiplied by the factorial of 5, the factorial of 5 is being calculated twice. Even worse, the factorial of 4 is being calculated three times. It makes far more sense to save those calculations and reuse them instead of starting over anew with each function call.

You can rewrite the factorial() function to make use of memoization in the following way:

function memfactorial(n){

    if (!memfactorial.cache){
        memfactorial.cache = {
            "0": 1,
            "1": 1
        };
    }

    if (!memfactorial.cache.hasOwnProperty(n)){
        memfactorial.cache[n] = n * memfactorial(n-1);
    }

    return memfactorial.cache[n];
}

The key to this memoized version of the factorial function is the creation of a cache object. This object is stored on the function itself and is prepopulated with the two simplest factorials: 0 and 1. Before calculating a factorial, this cache is checked to see whether the calculation has already been performed. No cache value means the calculation must be done for the first time and the result stored in the cache for later usage. This function is used in the same manner as the original factorial() function:

var fact6 = memfactorial(6);
var fact5 = memfactorial(5);
var fact4 = memfactorial(4);

This code returns three different factorials but makes a total of eight calls to memfactorial(). Since all of the necessary calculations are completed on the first line, the next two lines need not perform any recursion because cached values are returned.

The memoization process may be slightly different for each recursive function, but generally the same pattern applies. To make memoizing a function easier, you can define a memoize() function that encapsulates the basic functionality. For example:

function memoize(fundamental, cache){
    cache = cache || {};

    var shell = function(arg){
        if (!cache.hasOwnProperty(arg)){
            cache[arg] = fundamental(arg);
        }
        return cache[arg];
    };

    return shell;
}

This memoize() function accepts two arguments: a function to memoize and an optional cache object. The cache object can be passed in if you’d like to prefill some values; otherwise a new cache object is created. A shell function is then created that wraps the original (fundamental) and ensures that a new result is calculated only if it has never previously been calculated. This shell function is returned so that you can call it directly, such as:

//memoize the factorial function
var memfactorial = memoize(factorial, { "0": 1, "1": 1 });

//call the new function
var fact6 = memfactorial(6);
var fact5 = memfactorial(5);
var fact4 = memfactorial(4);

Generic memoization of this type is less optimal than manually updating the algorithm for a given function because the memoize() function caches the result of a function call with specific arguments. Recursive calls, therefore, are saved only when the shell function is called multiple times with the same arguments. For this reason, it’s better to manually implement memoization in those functions that have significant performance issues rather than apply a generic memoization solution.

Summary

Just as with other programming languages, the way that you factor your code and the algorithm you choose affects the execution time of JavaScript. Unlike other programming languages, JavaScript has a restricted set of resources from which to draw, so optimization techniques are even more important.

  • The for, while, and do-while loops all have similar performance characteristics, and so no one loop type is significantly faster or slower than the others.

  • Avoid the for-in loop unless you need to iterate over a number of unknown object properties.

  • The best ways to improve loop performance are to decrease the amount of work done per iteration and decrease the number of loop iterations.

  • Generally speaking, switch is always faster than if-else, but isn’t always the best solution.

  • Lookup tables are a faster alternative to multiple condition evaluation using if-else or switch.

  • Browser call stack size limits the amount of recursion that JavaScript is allowed to perform; stack overflow errors prevent the rest of the code from executing.

  • If you run into a stack overflow error, change the method to an iterative algorithm or make use of memoization to avoid work repetition.

The larger the amount of code being executed, the larger the performance gain realized from using these strategies.

Get High Performance JavaScript now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.