views:

603

answers:

6

I'm performance-tuning my code, and am surprised to find that the bottleneck is not dom node insert, but selection.

This is fast:

var row = jquery(rowHTML).appendTo(oThis.parentTable);

but the subsequent getting of an element inside "row" is slow:

var checkbox = jquery(".checkbox input", row);

I need to get the checkbox in every row so I can attach an event handler to it. Selecting the checkbox is ALMOST 10X AS SLOW as inserting the entire parent row.

What am I doing wrong here?

+1  A: 

Try putting a class name on the input field itself. That may prove to be faster.

The reason for that is your code goes through all .checkbox classes, tries to find the input child of that element and returns that. I think that action might be your culprit.

By simply just looking for all elements with the class the input field has, you might see some speedup.

Ólafur Waage
+2  A: 
var checkbox = jquery(".checkbox input", row);

This is traversing the entire dom tree to find the checkbox. You could possibly speed it up by changing the selector to an ID which can use the browsers native getElementById functionality.

var checkbox = jquery("#checkbox input", row);

You could also use your row as a starting point for the DOM search like the following example. Now your not parsing through the entire DOM tree again to find the matched element.

var row = jquery(rowHTML).appendTo(oThis.parentTable);
row.children(".checkbox input");
jfar
re: traversing the entire DOM tree - isn't this traversing only the child nodes of "row"?
annakata
I thought adding the second, "row" argument would prevent jquery from traversing the entire dom tree, and make it start just with my row element. Am I mistaken?
morgancodes
Hmmm, your right morgancodes, I'm going to do some tests. Are you primarily using table rows or divs?
jfar
Using "#checkbox input" as a selector is a bit of overkill - surely "#checkbox" would identify a single element, making the need to check for "input" completely redundant?
belugabob
+1  A: 

Try using Sly, it has an emphasis on performance.

RedFilter
A: 

Use event delegation and add a single handler to a parent element and not the checkboxes themselves.

jQuery supports this via the live() function.

Christoph
Great idea. It had never even occurred to me to approach event handling this way.
morgancodes
@morgancodes: I started to use event delegation nearly exclusively for bubbling events and add most of the listeners to document (the root of the event hierarchy); it might be inefficient (all listeners fire regardless of the event source), but it's extremely convenient, eg no need for onload hacks
Christoph
A: 

If you're looking for performance, jQuery selectors are very slow. In the example there, it has to scan the full DOM tree and check CSS classes and so on to find the relevant nodes.

It is significantly faster to use native DOM methods. There are some interesting library performance comparisons here:

http://ajaxian.com/archives/taskspeed-more-benchmarks-for-the-libraries-and-browsers

mdja
+6  A: 

DOM manipulation uses native functions to perform simple operations. Browser vendors optimize these. You are building the row from HTML. Internally jQuery is using .innerHTML to build the collection which then patches into the browser's mega-fast parser.

Selection is slow in comparison because JS code needs to loop through the DOM repeatedly. Newer browsers have native selection handling which provides dramatic speedups to selector based JS. As time moves on this will be less of a problem.

Here is how the query in question, $(".checkbox input", row), breaks down:

  1. row.getElementsByTagName('*');
  2. for-loop through every element returned (all elements within the row) and test elements[i].className with /(\s|^)checkbox(\s|$)/.
  3. for-loop every element still remaining and collect matched[i].getElementsByTagName('input');
  4. unique the final collection.

This is different for jQuery 1.3 as it's engine moves through the selector the other way around, beginning with getting all input elements and then testing the parent elements.

Rremember that the JS selector engines implement a lot more of the CSS selector spec than is actually usable with CSS (or implemented by current browsers). Exploiting this, and knowledge of the engines, we can optimize selector can be optimized in a few different ways:

If you know what element type the .checkbox is:

$("td.checkbox input", row);

It is faster for filter first for type and then for the class for only those matches. This doesn't apply for a very small subset of elements, but that is almost never the case in praxis.

The single class test is the slowest of the common selectors people actually use.

Simpler selection:

$("input[type=checkbox]", row);

One loop is faster than two loops. This only finds input elements and then directly filters them by type attribute. Since sub/child-elements are never used, unique may also be skipped (and smart engines will try to do this because unique is slow).

A more direct selector:

$("td:first.checkbox input", row);

A more complex selector may actually be faster if it is more direct (YMMV).

If possible, move the search context up to the table level:

By this I mean that instead of looping through the rows, and searching for the checkbox in every one, leave them alone until after the loop and then select them all at a time:

$("tr td:first.checkbox input", table);

The point of this is to eliminate the overhead of firing the selector engine up repeatedly, but instead do everything in one haul. This is presented here for completeness rather than something that I think would return massive speedups.

Don't select:

Build the row from bits, assigning events as you go.

var row = $( '<tr></tr>' );
var cell = $( '<td class="checkbox"></td>' ).appendTo( row );
$( '<input type="checkbox" name="..."/>' ).appendTo( cell ).click(/* ... */);

This may be impossible for reasons of Ajax or other templates out of your control. Additionally, the speed may not be worth turning your code into this sort of mess, but sometimes this may make sense.

Or, if none of these work for you, or return too performance gain, it may be time to rethink the method entirely. You can assign an event listener higher up the tree and grab the events there, instead of per-element instance:

$('table').change(function(e){
  // you may want a faster check...
  if ( $(e.target).is('input[type=checkbox]') ) {
    // do some stuff ...
  }
});

This way you don't do anything unless, and until, the user actually requests it. Fastest. :-)

Borgar
wow. long, great answer. I'll need a minute to digest this. Thanks Borgar!
morgancodes
I happen to have written an selector engine so this is an area that I happen to know a few things about. :-)
Borgar
Thanks for the in depth explanation
Gordon Tucker