Thursday, July 8, 2010

JavaScript Performance

Playing with client-side filtering. If we have a table with a number of rows, the goal is to allow a user type some text and if particular columns contain the typed text they will be left shown and the rest will be hidden (css display:none).



COL1COL2COL3
Value 1 to filter Value2 to filterValue3

First Attempt


We can have thousands of rows in a table. So iterate over all rows. for each row get its cells and then use indexOf on the cell innerText property to see if the string being searched for is in the table.

function FilterTable(tableName, text){
 var table = document.getElementById(tableName);
 var trs = table.getElementsByTagName('tr');
 var tds = [];
 if (text == '') {
 
  var tds = [];
  for (var i = 0; i < trs.length; i++) {
   trs[0].style.display = '';
  }
 }
 else {
  for (var i = 1; i < trs.length; i++) {
   trs[i].style.display = none;
   tds = trs[i].getElementsByTagName(td);
   index = tds[0].innerText.indexOf(text) + tds[1].innerText.indexOf(text);
   if (index > -1) {
    trs[i].style.display = '';
   }
  }
 }
}

I was only filtering by first and second column so I did not really iterate over every column.
The interesting thing is that this approach has a terrible terrible performance on large number of rows > 2500. The surprising thing is that even though performance is generally awful; it's even worse in Chrome than in IE8!!


Second Attempt


Playing with the above code, I noticed that the most expensive operation was actually not the indexOf call (although I didn't really use the chrome/ie8 profiler to verify this). The most expensive operation is accessing the DOM and changing properties. Namely this:
trs[i].style.display = 'none';

So in my second attempt I made some important changes:
  • in the search loop I instead saved the indices of the table rows whose cells contain the text for which the code is filtering the table. I saved those indices to an array. Later in the code I iterated quickly over the rows and set them all to hidden and one more pass over of the RowsToBeShown array and set those rows only to be visible.

  • The second change: instead of calling indexOf on two cells, I concatenated (at the server side) the contents of both cells and put them in the title property of the first cell. Now I have to call indexOf just once.

  • Another change is that I use jquery this time to select the cells to be filtered. I set the class name of the those cell to "FilterTarget".


function NewFilterTable(tableName,text)
{
  text = trim(text.toUpperCase());
     var table = document.getElementById(tableName);
     var tds = [];
     var none = 'none';
     var showthese = [];

     var j = 0;
     var td = 'td';
     var index = -1;
     if (text == '') {
         var trs = table.getElementsByTagName('tr');
         for (var i = 1; i < trs.length; i++) {
             trs[i].style.display = '';
         }
     }
     else {
         tds = $(".FilterTarget");         
         for (var i = 0; i < tds.length ; i++) {
             if (
             tds[i].title.indexOf(text) > -1 ) {
                 showthese.push(i);
             }
         }
     }
     for (var i = 0; i < tds.length; i++) {
         tds[i].parentNode.parentNode.style.display = none;
     }
     for (var i = 0; i < showthese.length; i++) {
         tds[showthese[i]].parentNode.parentNode.style.display = '';
     }
     return true;
}
Although I am still iterating twice over the tds array, moving the DOM accessing code out of the search helped performance big time. Withe first approach, Chrome would choke when the filter function is called and give the user a choice to stop executing the JavaScript code. IE does take very long time relatively speaking to run through the filtering code. With the seconds approach and with the same number of rows in the table, both IE8 and Chrome take about 3-5 seconds to run the function.
I am sure that are better ways/well-tested libraries that will do the filter much better but it's rather interesting what moving things around can do to performance. I will see what jQuery API can do here and also jQrid plugin for jQuery is awesome by the way for showing tables and doing an amazing number of things http://www.trirand.com/blog/
here the trim function


function trim(text) {
     return text.replace(/^\s+|\s+$/, '');
 }



I changed some variable names and minor things while typing up this post, so there might some js errors

No comments:

Post a Comment