Writing Effective jQuery Code: Optimization Techniques

This section focuses on a variety of simple optimization techniques to keep in mind when writing jQuery, or, in many cases, pure JavaScript code.

Be warned — once you start to pay strict attention to performance, it can infect all of your coding. Your users will love you.

1. Minimize DOM Updates

One of the most fundamental concepts of optimized front-end code is to keep the number of DOM updates in any given interaction down to the absolute minimum. Crossing over from the JavaScript engine into the DOM is a costly proposition. This is especially true if the operation is going to cause a repaint or reflow.

If you’re unfamiliar with the concept of reflows and repaints, pay particular attention to the next section. Understanding these concepts, how they differ, and how they affect both perceived and clock performance of a web page or application, is vital knowledge for the serious front-end engineer.

1.1. Reflows and Repaints

Two common, but potentially expensive operations in the life of a web page are reflows and repaints. Reflows and repaints happen when the browser needs to update its rendering model (the combination of the DOM and the CSS rules that make up the style component of the page). This happens at least once a page load (because the browser must draw something at least once), but can happen many more times in the case of dynamic applications.

The two events are related, but slightly different. Understanding the difference can make or break performance of a page:

  • A repaint is caused by a stylistic change to the page that doesn’t change the overall page geometry. Hiding an element or changing an element’s background color would cause a repaint.
  • A reflow is caused by a structural update to the page. Adding or removing an element from a document, changing its dimensions, or changing its display property can all cause a reflow. This is the more expensive of the two events because it involves calculating page geometry using the current DOM/CSS landscape.

1.2. Minimizing DOM Updates in Practice

You can see a dramatic and simple illustration of the performance penalty for DOM updates in Listing 10-1. The first example appends 10,000 rows to a table by calling $.append() during every iteration of the for loop. The second example creates a string, entirely in the JavaScript domain, and then writes the results to the DOM once, after the loop is completed.

LISTING 10-1: Minimizing DOM updates

for ( var i = 0; i < 10000; i + + ){

$( ”#main table” ).append( ”<tr><td>My job is to log table rows, this is row #

+i

+”</tr></td>” );

}

var tableRows= “”;

for ( var i=0; i < 10000; i++ ){

tableRows += “<tr><td>My job is to log table rows, this is row #”+i+”</tr></td>”;

}

$( “#main table” ).append( tableRows );

Code snippet is from minimize-dom-updates.txt

Looking at Table 10-1, you can see that the difference is dramatic, especially in Internet Explorer 8, where the example using multiple DOM updates takes nearly a full minute to complete.

1.3. Leverage DOM Hyperspace

Another, related way to improve DOM manipulations is to do them in what’s been referred to as “DOM Hyperspace.” Basically, this boils down to creating and/or manipulating DOM elements entirely on the JavaScript side of the bridge and crossing over to the DOM itself only once manipulations are complete. You can do this by creating loaded jQuery nodes as variables to be manipulated or by using $().detach() to pull elements out of the DOM to manipulate. In each case, the fragments are inserted after the operations are completed. The basic idea behind each technique is shown in Listing 10-2.

LISTING 10-2: Creating and manipulating elements in JavaScript

//Using detach to pull elements out of the document.

var $sideBar = $( “#sidebar” ).detach();

Populate the sidebar with menus, ads, and other miscellaneous elements

*/

$( “#main” ).append( $sideBar )

//Creating DOM elements with jQuery to later append to the document

var $sideBar = $( “<aside id=’sidebar></aside>” ).detach();

/*

Populate the sidebar with menus, ads, and other miscellaneous elements

*/

//insert it into the DOM

$( “#main” ).append( $sideBar )

Code snippet is from dom-hyperspace.txt

2. More Effective Looping

Before you get into this particular optimization, it should be pointed out that, most of the time, using the jQuery convenience method $.each() on a collection is a perfectly acceptable solution to iterating over the members of an array.

Sometimes, however, using $.each() isn’t acceptable. With larger collections where speed is an issue, using traditional control structures is a more effective solution.

2.1. $.each() and Array.forEach(): Trading Convenience for Speed

All implementations of this functionality, whether it’s the aforementioned jQuery convenience method or the EcmaScript 5th Edition Array method forEach(), are slower than an associated for loop. To understand why this is, it’s useful to understand the scope in which the looped code runs.

2.2. The Price of Function Creation and Adding Levels to Symbol Lookup

Take a look at the simple example in Listing 10-3 that looks through a collection of integers and tests to see if they’re multiples of the number 5. One test is done with a traditional for loop, one with [].forEach(), and the other is done with $.each().

LISTING 10-3: $.each() vs. [].forEach() vs. a traditional for loop

 /*

* Here’s the setup function which populates an array with integers

*/

var numbers = [],

fives = [];

for ( var i=0; i<1000000; i++ ){

numbers.push( Math.round( Math.random()*i ) );

}

/*

* First we use a traditional for loop,

* caching the length

*/

var test = numbers.length;

for ( var j = 0; j<test; j++ ){

if ( ( numbers[j] % 5 ) === 0    && numbers[j] !== 0 ) {

fives.push( numbers[j] );

}

}

/*

* Now we use the ES5 forEach method.

* This is actually what jQuery falls back on if it’s available

*/

numbers.forEach(

function( e,I ){

if ( ( e % 5 ) === 0 && e !== 0 ) {

fives.push( e );

}

}

)

/*

* Finally we use the $.each, convenience method.

*/

$.each( numbers,

function( i,e ){

if ( ( e % 5 ) === 0 && e !== 0 ) {

fives.push( e );

}

}

)

Code snippet is from looptest.txt

The difference in speed between the three versions is shown in Table 10-2. As you can clearly see, the for loop is significantly faster than the convenience methods.

The difference comes down to two things that, when understood, can help you write more effective JavaScript code. The first is being wary of extraneous function creation. Because they accept functions as arguments, the convenience methods require the JavaScript engine to create a new execution context for each step in the loop. Compare that to the traditional loop, which executes in the containing scope without the overhead of a new execution context.

Scope is the second piece. Because the operations are happening inside a function, any variables that reference the outer scope are now placed one level away in the scope chain. The looped code has to constantly walk the scope chain up a level to validate references. Because the traditional loop executes in one scope, any variables defined outside the loop still remain in the current scope.

3. Caching Objects

Building on the idea of using local variables to speed up your script, you’ll also want to think of caching objects in local scope to avoid additional script overhead. In general, if you’re going to use an object more than once in a function, it’s useful to cache that object in a local variable. It shortens lookup time, and if the variable is the result of a jQuery call, it cuts down on the number of times jQuery needs to do its selector magic.

It’s very common to need to do this with $(this) because the object bound to a function is often the subject of multiple operations in a function.

As a matter of style, when the object being saved to a local variable is a jQuery object, it’s also helpful to append a $ to the variable name. This indicates to anyone else on the project that it’s more than just a plain variable and has jQuery methods baked right in. Listing 10-4 shows a simple example that illustrates this technique on two separate variables.

LISTING 10-4: Caching objects for repeated use

var $this = $( this ),

$active = $( “.active” );

if ( $this.hasClass( “detail” ) ){

if ( $this.hasClass( “next” ) ){

$active

.toggleClass( “hidden” )

.removeClass( “active” )

.next( “.details” )

.toggleClass( “hidden” )

.addClass( “active” );

} else if ( $this.hasClass(“prev”) ){

$active

.toggleClass( “hidden” )

.removeClass( “active” )

.prev( “.details” )

.toggleClass( “hidden” )

.addClass( “active” );

}

}

Code snippet is from caching-objects.txt

4. Use Efficient Selectors

It’s not often that a 10-year-old article causes a ripple that spreads across the web development community, but that’s exactly what happened when people sat up and took notice of an article written in April 2000 by Mozilla engineer David Hyatt. Entitled “Writing efficient CSS for use in the Mozilla UI” (https://developer.mozilla.org/en/Writing_Efficient_CSS), the article lays out the way in which the browser parses the elements in a CSS selector and outlines several selector patterns that are particularly expensive.

The basic takeaways from this article are:

  • The engine evaluates each rule from right to left, starting with the “key” selector and moving through each selector until it finds a match or gives up.
  • Using fewer rules is generally more efficient.
  • Be as specific as possible — IDs are better than tags.
  • Avoid unnecessary redundancy.

What’s interesting is that JavaScript selector engines, including Sizzle and even the built-in document.querySelectorAll, also use some of the same techniques and also benefit from the same attention to efficiency.

In general, keeping your selectors short and sweet (leveraging IDs, for example) and keeping your key selectors as specific as possible will speed up your site whether you’re working with JavaScript or CSS.

Table 10-3 illustrates applying these general selector rules to jQuery. As you can clearly see, no matter what browser you’re using, leveraging IDs and single classes are by far the fastest ways to select an element. Anything more complicated than that is going to be much slower in every browser. It’s not always possible to use an ID or single class, but it’s fastest when you can.

4.1. jQuery-Specific Selector Performance

To be as efficient as possible, it’s also important to understand jQuery’s strategy for selectors. In general, jQuery wants to pass the selector immediately on to a native DOM method. As you saw in the previous section, if you’re using #id as a selector, jQuery passes that directly to document .getElementByld(“id”). As you saw in Table 10-3, that’s extremely fast. Selectors that can be passed along to document.getElementsByTagName and document.getElementsByClassName are also fast for the same reason.

As an aside, if you know your browser feature lists, you’ll know that the latter isn’t supported in older versions of IE, so that explains why class selectors are so slow in Internet Explorer 6 and 7.

document.querySelectorAll and jQuery Extensions

Selectors that can be passed to document.querySelectorAll (QSA) when it’s available are also very fast. The thing to keep in mind is that every available jQuery selector isn’t necessarily available in the native implementations of QSA. Because they’re hand-rolled implementations and not browser-optimized, these jQuery extensions to the CSS3 selectors are comparatively slow.

The jQuery extensions to the CSS3 selection engine are listed in Table 10-4. Use them with care. When using these selectors, it’s suggested that you pass a pure CSS selector to jQuery and then use .filter() to gather your specific results.

4.2. Give Your Selectors a Context

By default, when you pass a selector into jQuery, it traverses the entire DOM. There’s an underused, second possible context argument into jQuery that limits that search to a specific section of the DOM. As a note, it helps to pass in fast selectors as the additional context. It doesn’t much help to speed things up with a context if the context itself is slow to be reached.

This traverses the whole DOM:

$(“.className”);

This searches only element#id:

$(“.className”,”#id”)

As Table 10-5 shows, adding in a context is always faster than a whole DOM selector or a selector with two (or more) elements.

5. Consider Skipping jQuery Methods Entirely

If performance is of primary concern to your site or application, it’s useful to keep in mind the places where it’s safe to skip jQuery methods entirely, falling back to quicker core JavaScript methods and properties. One common example is using attr() to get the href attribute and then using attr() to apply it to another element:

$( “#baz” ).attr( “href”, $( this ).attr( “href” ) );

If you know core JavaScript methods, the preceding line can be rewritten as follows:

document.getElementById(“baz”).href = this.href;

As you can see, two calls to jQuery are removed because you use document.getElementByld to reference the element in question and then use direct lookups to the href attribute of each element instead of attr(). As Table 10-6 shows, even with this simple example, using native methods is faster in all major browsers.

Clearly, this isn’t going to be an option for every statement as there are cross-browser issues and you lose the ability to chain methods. If it was possible for everything, you wouldn’t be using jQuery in the first place. It’s just a good idea to know where you can fall back to the native methods to get a performance tweak. Simple property lookups to grab a string for later manipulation or to test against (this.id, this.className) are a common area, but look for examples in your own code. If performance becomes an issue, there might be easy gains waiting for you.

6. DRY

“Don’t repeat yourself” (or DRY) is an oft-repeated mantra in software development. It’s an important principle for maintenance and performance. With the simple elegance of the jQuery API, it’s important stylistically as well. One place where jQuery can encourage repetition is with the liberal use of anonymous functions. Because it’s so common to pass an anonymous function in as an event handler, it’s not uncommon to see several short functions doing the same thing in a block of code. Although this may feel faster and convenient in the short term, it’s in your best long-term interest to simplify the repeated code. You don’t want to wade through a thousand lines later to make edits to similar code that lives in five places in your application.

A typical example is shown in Listing 10-5. The function changes the class of the bound element, fires off an Ajax request, and then shows a jQuery UI dialog indicating the results of the request. Writing it once as an anonymous function makes sense. It’s short and full of common code. If it appears several times throughout your site, it’s time to simplify.

LISTING 10-5: Repeating yourself

$( “#foo” ).click(

$( this ).addClass( “active” );

$.get( “/my/app/service/”, function( data ){

$( “#dialog” ).text( data.status ).dialog( “open” );

}

};

);

$( “#bar” ).click(

$( this ).addClass( “active” );

$.get( “/my/app/service/”, function( data ){

$( “#dialog” ).text( data.status ).dialog( “open” );

}

);

$( “#baz” ).click(

$( this ).addClass(“active”);

$.get( “/my/app/service/”, function( data ){

$( “#dialog” ).text( data.status ).dialog( “open” );

}

);

Code snippet is from repeating-yourself.txt

You can simplify this code by collecting the selectors in use:

#baz, #bar, #foo” ).click(

$( this ).addClass( “active” );

$.get( “/my/app/service/”, function( data ){

$( “#dialog” ).text( data.status ).dialog(

}

);

Code snippet is from dry.txt

Alternatively, if the events are spread out across your site or application, you can create a callable function accessible from anywhere:

function ourHandler(){

$( this ).addClass( “active” );

download on $.get( “/my/app/service/”, function( data ){

$( “#dialog” ).text( data.status ).dialog( “open” )

}

} $( “#baz” ).click( ourHandler );

Code snippet is from dry-callable-function txt

Keeping code DRY takes a bit of vigilance, but once you start to recognize areas where it typically starts to creep in, you can stay ahead of it by immediately making the more maintainable solution.

Source: Otero Cesar, Rob Larsen (2012), Professional jQuery, John Wiley & Sons, Inc

Leave a Reply

Your email address will not be published. Required fields are marked *