Chapter 43. Optimizing Your Code

The applications you use every day have more than likely been in development for many years. The code behind them has been refined to make them more efficient and responsive to the user than they were at first. When first written, code is rarely the best it could be — usually far from it.

There's an old adage that most professional developers follow: "Make it run, and then make it run well." When you first write an application, don't worry about optimizing your code. Another adage is "Premature optimization is the root of all evil." Worry about making your application work, and after it works, then start optimizing your code.

There are three major aspects of JavaScript optimization. They are:

  • Refactoring code

  • Optimizing DOM code

  • Using event delegation

Let's start by looking at how to refactor your code.

REFACTORING CODE

It's been recounted many times, but it's worth repeating: The faster your code downloads, the faster your application feels to the user. You don't have control over your users' connection speed, but you do have control over how big your code files are. The size of your code files depends on a variety of factors, but two things that often add to code size are code duplication and unnecessary statements.

Refactoring Duplicate Code

You can cut the size of your code by refactoring duplicated code. When you refactor your code, you improve it without changing its external behavior. The most common means of refactoring code is identifying duplicated code and modifying it to be reused and easily maintained.

Recall the lesson on functions (Lesson 3). At the beginning of the lesson you were given an example of code duplication. It is listed here for your convenience:

var valueOne = 100;
valueOne = valueOne + 150;
valueOne = valueOne / 2;
valueOne = "The value is: " + valueOne; // results in "The value is: 125"

var valueTwo = 50;
valueTwo = valueTwo + 150;
valueTwo = valueTwo / 2;
valueTwo = "The value is: " + valueTwo; // results in "The value is: 100"

var valueThree = 2;
valueThree = valueThree + 150;
valueThree = valueThree / 2;
valueThree = "The value is: " + valueThree; // results in "The value is: 76"

This code performs the same exact operations on three different variables. Later in that lesson, that code was refactored to use the following function:

function augmentValue(originalValue) {
    var augmentedValue = originalValue;
    augmentedValue = augmentedValue + 150;
    augmentedValue = augmentedValue / 2;
    augmentedValue = "The value is: " + augmentedValue;

    return augmentedValue;
}

var valueOne = augmentValue(100); // results in "The value is: 125"
var valueTwo = augmentValue(50);  // results in "The value is: 100"
var valueThree = augmentValue(2); // results in "The value is: 76"

This refactor session saved two lines of code. Now, that may not seem like much right now, but let's look at the benefits. First, the code is reusable. If you need to augment more values in the same way, you can simply call the function as shown in the following example:

var valueFour = augmentValue(10); // results in "The value is: 80"
var valueFive = augmentValue(40); // results in "The value is: 90"
var valueSix = augmentValue(0); // results in "The value is: 75"

Now you begin to see real code savings from reusing the augmentValue() function. Before refactoring, each set of operations on one variable took four lines of code, including the variable declaration. If you were to perform the same set of operations on six variables, you'd do so in 24 lines of code. By refactoring the code you shrink the number of lines to 13 (seven for the function and six for each variable) — that's a nice savings.

Second, the code is more maintainable. Let's imagine that you wanted to add one to each value after dividing the number by two. Before refactoring you would have had to modify each set of operations like the bolded lines in the following code:

var valueOne = 100;
valueOne = valueOne + 150;
valueOne = (valueOne / 2) + 1;
valueOne = "The value is: " + valueOne; // results in "The value is: 125"

var valueTwo = 50;
valueTwo = valueTwo + 150;
valueTwo = (valueTwo / 2) + 1;
valueTwo = "The value is: " + valueTwo; // results in "The value is: 100"

// etc, etc, etc

Making changes like this makes your code more prone to error (what if you missed a variable?), and consumes too much time. You can achieve the same modification by editing one line of code, shown in bold in the following example:

function augmentValue(originalValue) {
    var augmentedValue = originalValue;
    augmentedValue = augmentedValue + 150;
    augmentedValue = (augmentedValue / 2) + 1;
    augmentedValue = "The value is: " + augmentedValue;

    return augmentedValue;
}

So by identifying duplicated code and refactoring it, you not only gain a performance increase by reducing the amount of code, you also save time and avoid potential errors.

Reducing Statements

The augmentValue() function in the previous section certainly eliminated duplicated code, but the function's body can be made to be more efficient and consume less space. The first change you can make is to remove the augmentedValue variable. The value passed to this function is supposed to be a number. Numbers are primitive values in JavaScript, so the value passed to this function is copied and contained within the originalValue variable. Because of this there's no need to copy the value in originalValue to augmentedValue. So the first refactor would result in the following:

function augmentValue(originalValue) {
    originalValue = originalValue + 150;
    originalValue = (originalValue / 2) + 1;
    originalValue = "The value is: " + originalValue;

    return originalValue;
}

You can refactor this function some more by combining statements. The JavaScript engine can execute one statement containing multiple operations faster than it can execute multiple statements performing one operation each. While you could combine all the statements in this function into one, a better approach (as far as maintainability is concerned) would be to combine statements that perform similar operations. For example, there are two statements that perform arithmetic operations. You can combine them into one statement, as shown in this code:

function augmentValue(value) {
    originalValue = ((value + 150) / 2) + 1;
    return "The value is: " + originalValue;
}

First, the originalValue parameter was changed to just value. This was done for two reasons:

  • Before the removal of the augmentedValue variable, the originalValue identifier made sense. Now, since augmentedValue is no longer used, originalValue is not meaningful.

  • Because originalValue is no longer meaningful, it makes sense to use the word value alone. It's short and saves space, while providing a meaningful name for the parameter.

Next, the first two statements are combined. This isn't always a good idea. If there are many mathematical operations to perform, they would be easier to read and maintain if they were in multiple statements. But here, combining them is fine.

Last, the concatenation and return statements are combined. Because there are no other operations that need to be performed after value is concatenated with a string, it makes sense to simply return the result of the concatenation operation.

Now the augmentValue() function is much smaller than it was before. That, combined with the refactoring of duplicated code into the augmentValue() function, makes this code efficient and ready for download.

Combining statements also applies to variables. Lesson 2 introduced the idea of initializing multiple variables within one statement as demonstrated in the following code:

var variableOne = "data",
    variableTwo = 10,
    variableThree = [];

Each variable initialization is separated by a comma. This code executes faster than the following code:

var variableOne = "data";
var variableTwo = 10;
var variableThree = [];

This book, for the sake of clarity, opted to declare and initialize multiple variables in separate statements like this code. Many professionals declare variables in this manner, but bear in mind that the single statement approach is more efficient.

OPTIMIZING DOM CODE

Refactoring code is only one piece of the optimization puzzle; you also need to look at your code that interacts with the DOM. As a JavaScript developer you'll spend a lot of your time working with the DOM — the slowest part of an application you'll deal with (other than connection-based parts like Ajax).

The DOM's slowness is primarily caused by two factors: re-rendering of the page caused by changes in the DOM, and the sheer amount of data the DOM manages. You can optimize your code that interacts with the DOM by limiting the number of updates you make to the DOM.

Using Document Fragments

In Lesson 14 you learned how to dynamically create HTML elements and text nodes with the document.createElement() and document.createTextNode() methods, and how to add them to the page with the appendChild() and insertBefore() methods. As a quick refresher, look at the following code:

for (var i = 0; i < 20; i++) {
    var el = document.createElement("p");
    el.appendChild(document.createTextNode("Paragraph " + i));
    document.body.appendChild(el);
}

This code creates 20 <p/> elements, adds a text node to them, and adds them to the page. Figure 43-1 shows you what it looks like in the browser.

Figure 43-1

Figure 43.1. Figure 43-1

There is technically nothing wrong with this code, but it is horribly inefficient because the DOM is updated 20 separate times, once for each <p/> element that is added to the page. With each update the browser has to redraw the page. This kind of page update can be noticeable by the user and inhibit performance. To solve this problem you want to reduce the number of updates made to the DOM, and you can do so by creating a document fragment.

Document fragments are lightweight document objects that can contain element objects that you can manipulate as if they were inside the HTML document. To create a document fragment you use the createDocumentFragment() method of the document object, like this:

var fragment = document.createDocumentFragment();

After you have created a fragment you can start adding element objects to it. In the case of this example you can append each <p/> element to a document fragment and then add the fragment to the HTML document. The following code illustrates this:

var fragment = document.createDocumentFragment();

for (var i = 0; i < 20; i++) {
    var el = document.createElement("p");
    el.appendChild(document.createTextNode("Paragraph " + i));
    fragment.appendChild(el);
}

document.body.appendChild(fragment);

Here, each <p/> element is appended to the document fragment. After the loop exits, the fragment is appended to the document's body node.

Using a document fragment in this way is advantageous because it reduces the amount of DOM updates to one. Even though you append each <p/> element to the fragment, the fragment exists outside of the DOM. So you don't cause an update to occur until you append the fragment to document.body. The document fragment object doesn't generate HTML itself, but its children do. So the resulting HTML in this code is exactly the same as it was without a document fragment.

Using innerHTML

Also in Lesson 14 you learned about the innerHTML property that you can use to add HTML to an element. Instead of creating node objects you simply construct a string containing HTML markup and assign it to an element's innerHTML property. It's simple, quick, and efficient — unless of course you make multiple updates to the DOM. Let's recreate the example from the previous section by using innerHTML to add <p/> elements to the page with the following code:

for (var i = 0; i < 20; i++) {
    var el = "<p>Paragraph " + i + "</p>";
    document.body.innerHTML = document.body.innerHTML + el;
}

As in the previous section, this code creates 20 <p/> elements and adds them to the document's body. As you might have realized, this code is inefficient because you make 20 updates to the body — causing the browser to render the page with every update. You could fix this by creating a variable and adding each HTML string to it, like this:

var html = "";

for (var i = 0; i < 20; i++) {
    html = html + "<p>Paragraph " + i + "</p>";
}

document.body.innerHTML = html;

But this code is inefficient too, because strings are immutable in JavaScript. The string value in html has to be copied, destroyed, and recreated with every loop iteration. A better solution is to create an array and push each HTML string to it, as shown in the following code:

var html = [];

for (var i = 0; i < 20; i++) {
    html.push("<p>Paragraph " + i + "</p>");
}

document.body.innerHTML = html.join();

In this code a string value is added as an element in the html array. After the loop exits, the html array's join() method is called to convert the array to a string value, which is then assigned to the body's innerHTML property. By building a HTML string, or in this case an array, you can limit the number of updates to the page.

USE EVENT DELEGATION

Events are a crucial part of a JavaScript application, and the more event handlers you use on a page the slower it responds to the user. Lesson 21 introduced you to event delegation — a technique by which you set a single event handler on a parent element to take advantage of event bubbling.

To reuse the example from Lesson 21, let's assume you want to handle the click event for two <div/> elements in a page. Here is the HTML:

<body>
    <div id="divElement"
        style="width: 100px; height: 100px; background-color: red; "></div>
<br/><br/>
    <div id="divElement2"
        style="width: 100px; height: 100px; background-color: red; "></div>
</body>

You want the color of these <div/> elements to change when they're clicked. You could add an onclick event handler to each element, but you also want to limit the amount of event handlers used in the page. To solve this problem, use event delegation by handling the document object's click event, like this:

var eventUtility = js24Hour.eventUtility;

eventUtility.addEvent(document, "click", function(event) {
var eSrc = eventUtility.getTarget(event);

    if (eSrc.tagName.toUpperCase() === "DIV") {
        var bgColor = eSrc.style.backgroundColor,
            color = "red";

        if (bgColor === color) {
            color = "green";
        }

        eSrc.style.backgroundColor = color;
    }
});

When the user clicks anywhere in the document, the event handler executes, and the code determines whether the element that received the mouse click is a <div/> element. If it is, its background color is changed.

Using event delegation is an efficient way to handle events in your page. Simply assign a single event handler for a particular event (click in this example), and use it to handle the same event for all elements in the page.

TRY IT

In this lesson, you learn how to optimize your code after you have written it by refactoring, limiting DOM updates, and using event delegation.

Lesson Requirements

You need a text editor. For Microsoft Windows users, Notepad is available by default on your system or you can download Microsoft's Visual Web Developer Express (www.microsoft.com/express/web/) or Web Matrix (www.asp.net/webmatrix/), both of which are free. Mac OS X users can use TextMate, which comes as part of OS X, or download a trial of Coda (www.panic.com/coda/). Linux users can use the built-in VIM.

You also need a modern web browser. Choose any of the following:

  • Internet Explorer 8+

  • Google Chrome

  • Firefox 3.5+

  • Apple Safari 4+

  • Opera 10+

You also need a webserver installed with PHP support. Refer to Lesson 31 on the DVD for installation instructions.

Create a subfolder called Lesson43 in your webserver's root directory. Copy all the files from the Lesson42 directory and paste them into the Lesson43 folder. Rename the lesson42_example01.htm file to lesson43_example01.htm. As an exercise in code optimization you'll refactor the ajaxUtility object.

Step-by-Step

  1. Open the ajaxUtility.js file in your text editor and look at it; specifically, look at the makeGetRequest(), makePostRequest(), and postFromForm() methods. They are listed here for your convenience:

    makeGetRequest : function(url, callback) {
        var xhr = this.createXHR();
    
        xhr.open("GET", url);
    
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                var status = xhr.status;
                if ((status >= 200 && status < 300) || status === 304) {
                    callback(xhr.responseText);
                } else {
                    alert("An error occurred");
                }
            }
        };
    
        xhr.send(null);
    }
    
    makePostRequest : function(url, data, callback) {
        var xhr = this.createXHR();
    
        xhr.open("POST", url);
    
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                var status = xhr.status;
                if ((status >= 200 && status < 300) || status === 304) {
                    callback(xhr.responseText);
                } else {
                    alert("An error occurred");
                }
            }
        };
    
        xhr.send(data);
    }
    
    postFromForm : function(url, form, callback) {
        var xhr = this.createXHR();
    
        xhr.open("POST", url);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                var status = xhr.status;
                if ((status >= 200 && status < 300) || status === 304) {
                    callback(xhr.responseText);
                } else {
                    alert("An error occurred");
                }
            }
        };
    
        xhr.send(this.getRequestBody(form));
    }

    There's a lot of code duplication here. All these methods create an XHR object, open the connection, assign the onreadystatechange event handler (which calls the callback function), and send the request. These operations can be refactored into their own method, so let's do it.

  2. Create a new method called openAndSend(). It will be responsible for creating the XHR object, opening a GET or POST request, setting the onreadystatechange event handler, and sending data (if applicable). So it should accept four arguments: the request type, the URL to send the request to, the data to send in the request, and the callback function. Its code follows:

    openAndSend : function(type, url, data, callback) {
        var xhr = this.createXHR();
    
        xhr.open(type, url);
    
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                var status = xhr.status;
                if ((status >= 200 && status < 300) || status === 304) {
                    callback(xhr.responseText);
                } else {
                    alert("An error occurred");
                }
            }
        };
    
        xhr.send(data);
    }

    Compare this code to the makeGetRequest() and makePostRequest() methods. They look very similar, don't they? In fact, the only differences are in the values passed to the XHR object's open() and send() methods.

  3. So let's refactor makeGetRequest() and makePostRequest() to call openAndSend(). Following is the new code:

    makeGetRequest : function(url, callback) {
        this.openAndSend("GET", url, null, callback);
    },
    
    makePostRequest : function(url, data, callback) {
        this.openAndSend("POST", url, data, callback);
    },

    In makeGetRequest(), openAndSend() is called by passing four things: the string "GET" for the request type, the url parameter, null for the data, and the value passed to the callback parameter. The makePostRequest() was altered to pass "POST", as well as the url, data, and callback parameters.

    Notice how these two methods did not change from an API perspective; you still call makeGetRequest() and makePostRequest() as you did before. But underneath the hood these methods do something different by calling the new openAndSend() method to do all the work. When you're refactoring code that's already in use, it's important to do your best to keep the external portions the same. That way you can drop in the newly revised ajaxUtility.js file without having to rewrite any code relying on the ajaxUtility object. For example, when you're finished refactoring this code you can use it in any example in this book that uses the ajaxUtility object, without having to make any additional changes.

  4. Now let's look at the postFromForm() method. It's the same as the makeGetRequest() and makePostRequest() methods except for one small detail: It sets a Content-Type header using the XHR object's setRequestHeader() method. At first glance it looks as if we can't refactor this method the way you did the other two. Fortunately, however, you can.

    The only thing postFromForm() does is set that header, so you can add another parameter to openAndSend() to determine whether or not the Content-Type header is set — essentially refactoring again. Following is the revised openAndSend() with the changes in bold:

    openAndSend : function(type, url, data, callback, contentType) {
        var xhr = this.createXHR();
    
        xhr.open(type, url);
    
        if (contentType) {
            xhr.setRequestHeader("Content-Type", contentType);
        }
    
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                var status = xhr.status;
                if ((status >= 200 && status < 300) || status === 304) {
                    callback(xhr.responseText);
                } else {
                    alert("An error occurred");
                }
            }
        };
    
        xhr.send(data);
    }

    In this revision, the contentType parameter is a string value (a content type). If data is passed to it, the value passed to the contentType argument is used to set the Content-Type header using the XHR object's setRequestHeader(). If omitted, the header code is skipped and the method continues executing as normal.

  5. Now change postFromForm() to call openAndSend(), like this:

    postFromForm : function(url, form, callback) {
        this.openAndSend("POST", url, this.getRequestBody(form), callback,
            "application/x-www-form-urlencoded");
    }

    Even though you changed the parameter list of openAndSend(), you do not have to change the makeGetRequest() and makePostRequest() methods. Parameters in JavaScript are always optional. You technically don't have to pass anything to a function in order to execute it, but the function may not do what it's supposed to do if you do not provide it with the data it needs.

    When an argument is not passed, the parameter contains a value of undefined. So when makeGetRequest() and makePostRequest() call openAndSend() without passing a value as the contentType parameter, contentType becomes undefined, a false value.

    So once again your refactor session changed the way something worked without causing you to rewrite the code that depended on it.

  6. The ajaxUtility.js file should now look like this:

    js24Hour.ajaxUtility = {
        createXHR : function() {
            if (typeof XMLHttpRequest !== "undefined") {
                return new XMLHttpRequest();
            } else {
                var versions = [ "MSXML2.XmlHttp.6.0",
                    "MSXML2.XmlHttp.3.0" ];
    
                for (var i = 0; i < versions.length; i++) {
                    try {
                        var xhr = new ActiveXObject(versions[i]);
                        return xhr;
                    } catch (error) {
                        // do nothing
                    }
                }
            }
    
            alert("Your browser does not support XmlHttp");
    
            return null;
        },
    
        getRequestBody : function(form) {
            var pieces = [],
    elements = form.elements;
    
            for (var i = 0; i < elements.length; i++) {
                var element = elements[i],
                    name = encodeURIComponent(element.name),
                    value = encodeURIComponent(element.value);
    
                pieces.push(name + "=" + value);
            }
    
            return pieces.join("&");
        },
    
        makeGetRequest : function(url, callback) {
            this.openAndSend("GET", url, null, callback);
        },
    
        makePostRequest : function(url, data, callback) {
            this.openAndSend("POST", url, data, callback);
        },
    
        postFromForm : function(url, form, callback) {
            this.openAndSend("POST", url, this.getRequestBody(form), callback,
                "application/x-www-form-urlencoded");
        },
    
        openAndSend : function(type, url, data, callback, contentType) {
            var xhr = this.createXHR();
    
            xhr.open(type, url);
    
            if (isForm) {
                xhr.setRequestHeader("Content-Type", contentType);
            }
    
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    var status = xhr.status;
                    if ((status >= 200 && status < 300) || status === 304) {
                        callback(xhr.responseText);
                    } else {
                        alert("An error occurred");
                    }
                }
            };
    
            xhr.send(data);
        }
    };
  7. Save ajaxUtility.js. Let's test it to make sure everything works as it should. Point your browser to http://localhost/Lesson43/lesson43_example01.htm. Fill out the form, submit it, and you'll see that it works exactly as it should.

Note

Please select Lesson 43 on the DVD to view the video that accompanies this lesson. You can also download the sample code for all lessons from the book's website at www.wrox.com.

Step-by-Step
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.12.207