Tuesday, November 14, 2006

javascript table creation benchmarks

JavaScript Table Creation Benchmarks

The purpose of this exercise is to find out the fastest way to create html tables with javascript.

I am testing 3 methods of creating a table: pure DOM, strings with innerHTML, and a DOM + innerHTML hybrid, where an off-screen table is constructed using strings and then the rows are copied into the body of the target table. Each test is run with 2 options: with and without content, to estimate which method is better for creating empty tables and populating them later as opposed to creating the tables where the content is known at the time of creation. To benchmark real-world performance, the end time is take after a small timeout to allow the table to render. Additionally, each test is run with and without styles to measure the impact of some common css rules.

Test 1. DOM.

Wrappers around the test function:
function domTestWithContent(cnt) {
    domTableTest(cnt, true);
}

function domTestWithoutContent(cnt) {
    domTableTest(cnt, false);
}

Using cloneNode() whereever possible to avoid the overhead of document.createElement. This turns out quite a bit faster than body.insertRow and row.insertCell. The difference between creating the table with and without content is quite big: setting the data requires a loop through all the cells after the table is constructed.

/** 
    cnt: an element where to place the result
    setCellContent: pass true to populate cells with "row, column" 
*/
function domTableTest(cnt, setCellContent) {
    if (typeof(setCellContent) == "undefined")
        setCellContent = false;

    var tab = document.createElement("TABLE");
    var bod = document.createElement("TBODY");
    tab.appendChild(bod);
    for (var r = 0; r < rows; r++) {
        if (r == 0) {
            var row = bod.insertRow(-1);
            for (var c = 0; c < columns; c++) {
                if (c == 0) {
                    var cell = row.insertCell(-1);
                    if (setCellContent)
                        cell.appendChild(document.createTextNode("\u00A0"));
                }
                else {
                    cell = row.firstChild.cloneNode(true);
                    row.appendChild(cell);
                }
                if (setCellContent)
                    cell.firstChild.nodeValue = r + ", " + c;
            }
        }
        else {
            var row = bod.firstChild.cloneNode(true);
            if (setCellContent) {
                var cell = row.firstChild;
                var c = 0;
                while (cell) {
                    cell.firstChild.nodeValue = r + ", " + c;
                    cell = cell.nextSibling;
                    c++;
                }
            }
            bod.appendChild(row);
        }
    }
    cnt.appendChild(tab);
}

Test 2. innerHTML.

Wrappers around the test function:
function stringTestWithContent(cnt) {
    stringTableTest(cnt, true);
}

function stringTestWithoutContent(cnt) {
    stringTableTest(cnt, false);
}

Using arrays to speed up strings performance. Push() may not be the fastest way to add array elements but I believe the overhead is negligible in this context. There is practically no difference between populating the cells with real data or blanks.

/*
    cnt: the element where to place the result
    setCellContent: send true to populate cells with "row, column"
*/
function stringTableTest(cnt, setCellContent) {
    if (typeof(setCellContent) == "undefined")
        setCellContent = false;
    
    var buffer = new Array();
    buffer.push("");
    buffer.push("")
    for (var r =0; r < rows; r++) {
        buffer.push("");for (var c =0; c < columns; c++) {
            buffer.push("");}
        buffer.push("");}
    buffer.push("
"); if (setCellContent) buffer.push(r + ", " + c); else buffer.push(" "); buffer.push("
"); cnt.innerHTML = buffer.join(""); }

Test 3. innerHTML + DOM.

Wrappers around the test function:
function hybridTestWithContent(cnt) {
    stringTableTest3(cnt, true);
}

function hybridTestWithoutContent(cnt) {
    stringTableTest3(cnt, false);
}

If innerHTML turns out to be much faster than dom, this method will be good for adding rows to an existing table.

function hybridTableTest(cnt, setCellContent) {
    if (typeof(withstyle) == "undefined")
        withstyle = false;

    var tab = document.createElement("TABLE");
    cnt.appendChild(tab);

    var buffer = new Array();
    buffer.push("")
    for (var r =0; r < rows; r++) {
        buffer.push("");for (var c =0; c < columns; c++) {
            buffer.push("");}
        buffer.push("");}
    buffer.push("
"); if (setCellContent) buffer.push(r + ", " + c); else buffer.push(" "); buffer.push("
"); // create the table in an off-screen div var node = document.createElement("DIV"); var s = buffer.join(""); node.innerHTML = s; // copy rows var oldB = document.createElement("TBODY"); tab.appendChild(oldB); var newB = node.firstChild.firstChild; for (var i = 0; i < rows; i++) { var r = newB.firstChild; oldB.appendChild(r); } }

Running the tests

The settings: which tests to run, how many rows & columns to create, etc
var rows = 300;
var columns = 30;
var loops = 3;
var needStyles = false;
var tests = [   domTestWithContent,     domTestWithoutContent,
                stringTestWithContent,  stringTestWithoutContent, 
                hybridTestWithContent,  hybridTestWithoutContent
            ];
Using recursion and timers instead of a while or for loop to allow each test to complete rendering before proceeding with next iteration. There may be an overhead of a few milliseconds associated with setTimeout(), but it is the same for each run and it is negligible compared to the time taken by the test itself.
function run() {
    var contentElement = document.getElementById("cnt");
    // set (or unset) the class of the target element, to test with or without styles 
    setStyle(contentElement, "withstyle", needStyles);
    var testTimes = new Array(tests.length);
    for (var j = 0; j < testTimes.length; j++)
       testTimes[j] = 0;
    var i = 0;
    var loop = 0;
    
    // this function will be invoked to run each test
    runOneTest = function() {
        // empty the target element
        while (contentElement.firstChild)
            contentElement.removeChild(contentElement.firstChild);
        // start time
        var d1 = (new Date()).getTime();
        // run the test
        tests[i](contentElement);
        
        setTimeout(function() {
            // take the 2nd time measurement after rendering is done
            var d2 = (new Date()).getTime();
            testTimes[i] += (d2 - d1);
            
            i++;
            if (i < tests.length)
                // run the next test in tests array
                runOneTest();
            else if (loop < loops) {
                // run the tests again 
                loop++;
                i = 0;
                runOneTest();
            }
            else {
                // calculate averages and show results
                for (var k = 0; k < testTimes.length; k++)
                    testTimes[k] = Math.round(testTimes[k]/loops);
                showResults(testTimes);
                if (!needStyles) {
                    // chain the 2nd test
                    // not pretty but does the trick
                    needStyles = true;
                    run();
                }
            }
        }, 1);
    };
    
    runOneTest()
}

This is the stylesheet applied when testing with styles. Nothing fancy.
.withstyle {
}

.withstyle TABLE {
  border-collapse: collapse;
} 

.withstyle TD {
   border: solid 1px black;
   white-space: nowrap;
   width: 55px;
   font-family: Sans-serif;
   font-size: 10px;
}
Run the tests
run();

Results

There results are only good for comparisons with one another and not as any kind of absolute performance guideline. So it makes very little sense to mention what kind of hardware/os combo I got them on. But I will anyway.

These numbers are from a dual Xeon 3.6 Ghz workstation with 3GB of ram and no hyper-threading. Windows XP.

style? DOM w/content DOM w/o content innerHTML w innerHTML w/o hybrid w hybrid w/o
opera
w/o 787 427 734 625 781 609
w 1073 557 1047 927 1099 896
firefox 2
w/o 1843 359 1276 802 1198 833
w 1781 463 896 891 1078 911
firefox 1.5
w/o 1906 375 1193 802 1229 901
w 1651 495 995 922 1047 974
internet explorer 6
w/o 2406 1636 1177 766 1609 1198
w 7145 6583 5828 5370 6218 5994

Conclusions

Go Opera. Too bad it's irrelevant. However, on most tests firefox is not far behind, and in the tests with style it is actually ahead. This is, in fact, very suprising - Firefox shows better numbers when styles are set, while common sense tells me that it should be a bit slower, like the rest of the browsers.

What a pathetic display from Internet Explorer. Whoever said it was fast probably never waited for it to finish rendering. Or never tried formatting the pages. Or both. I hope IE7 is better

Internet Explorer screws up all conclusions. It is clear to me that for Firefox and Opera, DOM is the way to go, especially with empty tables. In IE innerHTML is the king. Again, IE7 will hopefully change this.

Run the test here: http://www.benya.com/code/jsbenchmarks/tables.html and post your results. I'd love to see how Safari fares, especially compared to some other browser running on the same platform.

No comments: