Monday, February 7, 2011

JavaScript/jQuery code optimization (or famous IE bottlenecks)

After we pulled out the Infragistics grid in favor of Telerik MVC Extensions, We've had to code several features, that were available in Infragistics out of the box, ourselves. This was partly due to our data structure not being fully supported by the Telerik grid. That's a topic for another blog post though. One of the features was the ability to select several cells and do spreadsheet like functionality, such as copy/paste and fill. We were mostly testing this out in Chrome/Firefox and with small tables. However, as typically happens, after releasing into our demo environment, our BA told us that the performance was brutal.

Surprise, surprise, it turned out that our grid page ran very slowly in IE(7) with very large tables (50 rows by 35 columns). I wound up being able to trace the bottle neck down to this chunk of code.
$('tbody td', $(this.element)).each(function() {
 if (this.parentElement.rowIndex >= selection.startRowIndex && 
 this.parentElement.rowIndex <= selection.endRowIndex &&
 this.cellIndex >= selection.startColIndex && 
 this.cellIndex <= selection.endColIndex) {
  if (columns[this.cellIndex].readonly != true) {
   $(this).addClass('selected');
  }
 }
});
This is the code for when a user has a cell selected and is either dragging the mouse or holding shift and clicking to select a block of cells. What's happening here is that we're going through all cells in the table body and seeing if it is between the first selected cell and the cell located at the mouse position. I.E. whether or not the cell is in a 'box'. The problem is that we're going through every cell in the table body, even if only one has been selected.

The first optimization was to only go through the rows/columns that were in the selected box, so to speak. This was done using the jQuery slice method. The code is below

$('tbody tr', this.element)
 .slice(selection.startRowIndex, selection.endRowIndex + 1)
 .each(function() {
  $(this).children('td')
   .slice(selection.startColIndex, selection.endColIndex + 1)
   .each(function() {
    if (!columns[this.cellIndex].readonly) {
     $(this).addClass('selected');
    }
   });
 });

The performance improved a lot when the selection box was small, but if the user selected anything bigger than a 4 row by 10 column box the script crawled again. Each row would take approximately 190 ms to run in IE7. So if the user selected 10 rows it would take almost 2 seconds to run. The problem seemed to be the readonly check. If it was taken out, the timing dropped to a fraction of a millisecond.

The problem is that Internet Explorer is very slow processing conditional statements. Doing some unscientific tests on this site I got around the following numbers:

BrowserAverage Time
Chrome 80.02 ms
Firefox 4 b100.66 ms
Internet Explorer 852 ms
Internet Explorer 794 ms

So we had to find a way to optimize that conditional statement. I finally found a jQuery call that seemed to do the trick.

$('tbody tr', this.element)
 .slice(selection.startRowIndex, selection.endRowIndex + 1)
 .each(function() {
  $(this).children('td')
   .slice(selection.startColIndex, selection.endColIndex + 1)
   .not(function() {
    return columns[this.cellIndex].readonly;
   })
   .addClass('selected');
   });

After going through the jQuery code, I'm still not 100% sure why the performance is better with the last statement. A couple of things I've noticed is that using each to do a check on every item seems to be slightly more expensive than not (or for that matter filter). Since each also does a check to make sure the callback hasn't returned false. Also addClass has a lot of overhead so it's quite inefficient to run it on elements one by one. Not sure if that's necessarily the problem either though because the 2nd code snippet ran very quickly when the readonly check was taken out.

If anyone has any ideas on why they think the 3d code snippet runs much faster than the 2nd one I would love to hear it. Until then this just another example of why it's important to optimize your jQuery code.