Web DevCenter
oreilly.comSafari Books Online.Conferences.
MySQL Conference and Expo April 14-17, 2008, Santa Clara, CA

Sponsored Developer Resources

Web Columns
Adobe GoLive
Essential JavaScript
Megnut

Web Topics
All Articles
Browsers
ColdFusion
CSS
Database
Flash
Graphics
HTML/XHTML/DHTML
Scripting Languages
Tools
Weblogs

Atom 1.0 Feed RSS 1.0 Feed RSS 2.0 Feed

Learning Lab






Dynamic HTML Tables: Improving Performance
Pages: 1, 2

Approach II: Generic DOM Node Creation

One aspect of the W3C DOM specification that has long bugged me is the verbosity of the syntax required to accomplish simple tasks. Document-tree modifications frequently require several baby steps, including multiple object creations and method calls, plus the occasional twist to deal with the parent-node-centric view of the world (where an operation on content must be performed from the scope of the containing parent element). When designing for an interpreted execution environment (especially one in which users suffer a download penalty for bloated source code), the performance-aware programmer tries to minimize the number of statements that execute to accomplish a given task. Therefore, I initially didn't have much hope for the wordy way of creating all of the objects for the table (accumulating them along the way in memory), and then stuffing the results into the existing tbody element on the page.



One way to simplify the process, however, is to accumulate the content in a DocumentFragment object, a handy addition to the W3C DOM specification. A DocumentFragment node can act as a catchall container (in memory, not in the document tree) for other nodes that you are assembling. As a genuine DOM node, it has all of the properties and methods shared by all node types. But then it does something magical: when you append or insert the DocumentFragment container node into an existing document tree, the container goes away, and its contained nodes go into the designated location within the document tree. The DocumentFragment node is supported by IE 6 for Windows (but no earlier), IE 5 for Macintosh, all Mozilla-based browsers, Opera 7, and Safari (and perhaps others).

The code for creating document fragment-based content for our tbody element expands substantially over the Approach I code when creating the individual tr and td elements:

// prepare references to document tree objects for later
var mytable = document.getElementById("myTable");
var mytbody = document.getElementById("myTbody");
// generate new node to be assembled in memory
var myNewtbody = document.createElement("tbody");
myNewtbody.id = "myTbody";
// generate fragment container for tbody assembly
var docFragment = document.createDocumentFragment();
var trElem, tdElem;
for (var j = 0; j < tableData.length; j++) {
   trElem = document.createElement("tr");
   trElem.className = "tr" + (j%2);
   
   tdElem = document.createElement("td");
   tdElem.className = "col0";
   tdElem.innerHTML = tableData[j].alpha;
   trElem.appendChild(tdElem);
   ...
   tdElem = document.createElement("td");
   tdElem.className = "col4";
   tdElem.innerHTML = tableData[j].epsilon;
   trElem.appendChild(tdElem);
   
   docFragment.appendChild(trElem);
}
myNewtbody.appendChild(docFragment);
// blast new tbody into the document tree table
mytable.replaceChild(myNewtbody, mytbody);

Revising the code to adhere more strictly to the W3C DOM text-node creation technique fills out the code even more (changes shown in bold):

var mytable = document.getElementById("myTable");
var mytbody = document.getElementById("myTbody");
var myNewtbody = document.createElement("tbody");
myNewtbody.id = "myTbody";
var docFragment = document.createDocumentFragment();
var trElem, tdElem, txtNode;
for (var j = 0; j < tableData.length; j++) {
   trElem = document.createElement("tr");
   trElem.className = "tr" + (j%2);

   tdElem = document.createElement("td");
   tdElem.className = "col0";
   txtNode = document.createTextNode(tableData[j].alpha);
   tdElem.appendChild(txtNode);
   trElem.appendChild(tdElem);
   ...
   tdElem = document.createElement("td");
   tdElem.className = "col4";
   txtNode = document.createTextNode(tableData[j].epsilon);
   tdElem.appendChild(txtNode);
   trElem.appendChild(tdElem);
   
   docFragment.appendChild(trElem);
}
myNewtbody.appendChild(docFragment);
mytable.replaceChild(myNewtbody, mytbody);

Despite the substantial amount of processing that occurs inside of the repeat loop of the node-intensive approach (compared to the table-specific methods of Approach I), this last way of assembling tbody content and populating an existing table proved to be the fastest. In timing tests, it consistently outpaced all approaches shown earlier, in all tested browsers except Safari. Text node creation and appendage was faster than assigning string values to innerHTML properties of td elements. The exception was Opera 7, which slightly favored the innerHTML content approach -- but Opera was a comparative speed racer in the document-fragment derby, significantly outpacing other browsers.

Approach III: Filling Table Segment via innerHTML

A seemingly logical alternative approach to try is to accumulate the tbody element's content as an HTML string, and then assign that string to the innerHTML property of the tbody element in the document tree's table. The main benefit is that you don't have to clear out the old content before inserting the new: it's a flat-out replacement. Unfortunately, Internet Explorer for Windows does not support the innerHTML property for the tbody element (or several other abstract elements, for that matter). Mozilla browsers do support the property in table section elements, so it was worth comparing this technique to the others. In tests with Mozilla 1.2 on the Macintosh, this approach was essentially equal to the fastest node-assembly technique; Netscape 7 under Windows, however, was incredibly fast with this technique, operating at more than twice the speed of the node assembly technique of Approach I.

Without support for IE/Windows, however, assigning a string to a tbody element's innerHTML property is a non-starter for cross-browser implementation.

True Cross-Browser Approaches

To include Internet Explorer for Macintosh in the action, it's safest to recreate the entire non-nested table from scratch each time you want to display a different version of the table. In trying several of the techniques described earlier on a table element nested inside a div container, the one that IE/Mac handles best is accumulating the HTML as a string, and assigning the result to the innerHTML property of the table's next outermost div container:

var output = "<table id='myTable1'><tbody id='myTbody'>";
for (var j = 0; j < tableData.length; j++) {
   output += "<tr class='tr" + (j%2) + "'>";
   output += "<td class='col0'>" + tableData[j].alpha + "</td>";
   output += "<td class='col1'>" + tableData[j].beta + "</td>";
   output += "<td class='col2'>" + tableData[j].gamma + "</td>";
   output += "<td class='col3'>" + tableData[j].delta + "</td>";
   output += "<td class='col4'>" + tableData[j].epsilon + "</td></tr>";
}
output += "</tbody></table>";
document.getElementById("tableWrap").innerHTML = output;

As for overall performance of this approach, it performs well enough across all browsers, although it's not necessarily the fastest possible in IE for Windows or Safari, and a bit sluggish in Opera. But as a fully cross-browser compromise -- and a compromise to some because it uses the de facto innerHTML standard, rather than the W3C DOM standard -- this technique "does the job." The other way of generating an entire table element (via DOM node creation in memory) didn't fare well in IE/Mac. The browser begins to bog down terribly with apparent resource difficulties as the new table element in memory (not even in the document tree) reaches a couple of hundred rows.

About Deleting Nodes

While testing these table-creation techniques, it was also helpful to include related test code that deleted the previous table to prepare for insertion of a DocumentFragment node for Approach II. If you are planning to display re-sorted table content, you can delete the current contents and reuse the same table generation function that displays the table in the first place.

The simplest way to cleanse the table is to delete individual child nodes of a parent container until no child nodes remain, leaving the container clear to append its new content, as in the following:

function clearTbody() {
   var tbody = document.getElementById("myTbody");
   while (tbody.childNodes.length > 0) {
      tbody.removeChild(tbody.firstChild);
   }
}

The question, however, is whether there is any advantage to deleting the first or last child inside of this potentially long repeat loop. In many timing tests on various browsers, the performance edge went to repeatedly deleting the first child node until all children are gone. Opera 7, however, chugged slowly at this task, regardless of the end from which you remove child nodes.

Conclusion

The most important lesson I learned from this exploration is not to fear the extra statements and seemingly "expensive" node-object creation of the pure W3C DOM model for generating elements and text content (at least for tables). Assembling DOM objects in memory is commonly more efficient than extensive string concatenation and assignment to innerHTML properties. Not surprising, of course, is that you want to avoid modifying the document tree repeatedly (as shown in the table-specific methods of Approach I), especially since it bogs down terribly under the strain of a huge amount of direct table manipulation.

Finally, in a pinch (when Internet Explorer for Macintosh must be a part of your audience mix) you can still implement dynamic tables if you operate on the entire non-nested table and use pure string HTML accumulation techniques. If Apple's Safari browser should replace IE as the dominant web client on the Mac, then the scripting job gets easier, because Safari's W3C DOM implementation already handles table modifications with ease.

Editor's note: In response to feedback from our readers, Danny Goodman has provided a working example that lets you run your own timing tests. Click here to view the page.

Danny Goodman has been writing about technology and computers full-time since 1981 and is the author of Dynamic HTML: The Definitive Reference and "The Complete HyperCard Handbook."


O'Reilly & Associates recently released (April 2003) JavaScript & DHTML Cookbook.


Return to the Web Development DevCenter.


  • Tuned method can fail in Opera 7.22
    2003-11-24 11:38:31  anonymous2 [View]

    The join() methods fails in Opera 7.22 when the array length exceeds 9999.
    It returns an 'undefined' value.

    This means you run into problems when you have a 200 x 50 or a 500 x 20 table and you push() each cell individually to the array.

    Hubert

    hubert.kauker@web.de
  • Even faster
    2003-08-07 15:11:21  jconradmn [View]

    1. Scalability of Array
    I found that by boosting the count from 500 to 1000, you see even more of a pronounced difference in speed. The large string in 4 goes from (your numbers may vary) 3 s to 19 s (6x). That scales faster than using an array, which scales from .3 s to .9 s (3x).

    2. Declining benefit of Array
    Using an array helps. For me, building a string array was about 9 times faster than using the string addition in #4. However, I also saw an increase of 120 ms by using a hybrid. That is, using one array element per TR string instead of one for each TD.

    3. Having the line in javascript be a single line, if you can bear with the loss of readability, contributed another 50 ms reduction.

    Here is my reduced function,

    // Approach #4f
    // Assemble entire table element as fewest HTML strings as part of an array.
    // Also saving object into a variable during each iteration.
    // Also using a while decrement.
    // jeff.conrad@lawson.com

    function makeTableApproach4f() {
    clearTimers();
    if (document.body.innerHTML) {
    document.getElementById("deleteTime").firstChild.nodeValue = "not applicable";
    timer.createLoopStart = new Date();

    var output = new Array();

    var t = null;
    var j = tableData.length;
    while(--j >= 0)
    {
    t = tableData[j];
    output [j] = "<tr class='tr" + (j%2) + "'><td class='col0'>" + t.alpha + "</td><td class='col1'>" + t.beta + "</td><td class='col2'>" + t.gamma + "</td><td class='col3'>" + t.delta + "</td><td class='col4'>" + t.epsilon + "</td></tr>";
    }

    var table = "<table id='myTable1'><tbody id='myTbody'>"
    + output.join("")
    + "</tbody></table>"
    ;
    output = null;
    timer.populateStart = new Date();
    document.getElementById("tableWrap2").innerHTML = table;
    timer.end = new Date();
    showTimes("Approach #4f");
    } else {
    alert("This test not supported in the current browser.");
    }
    }

    Thanks for this article, it is very timely.
    • Even faster
      2003-11-04 06:00:16  anonymous2 [View]

      Yes, Jeff, you are obviously right.

      So let us build long strings in single string expressions, however complex they may be,
      and put them into as few array elements as possible.
      Using a 'while' loop instead of 'for' might make a small difference, too.
      But I would still consider using the push() method.

      This approach does not work well, however, when we have an unknown or variable number of table columns, because entire table rows cannot be processed at a time.

      // Allocate temporary array.
      var output = new Array();

      // Transfer table data to array.
      for (var j = 0; j < tableData.length; j++)
      {
      // Access j-th row of data.
      var t = tableData[j];

      // Process entire table row.
      output.push(
      "<tr class='tr" + (j%2) +
      "'><td class='col0'>" + t.alpha +
      "</td><td class='col1'>" + t.beta +
      "</td><td class='col2'>" + t.gamma +
      "</td><td class='col3'>" + t.delta +
      "</td><td class='col4'>" + t.epsilon +
      "</td></tr>"
      );
      }

      // Produce html output.
      document.getElementById("tableWrap").innerHTML =
      "<table id='myTable1'><tbody id='myTbody'>" +
      output.join("") +
      "</tbody></table>" ;

      // Release.
      output = null;

      Hubert Kauker
  • More tweaks
    2003-07-24 08:29:35  anonymous2 [View]

    Huberts code is significantly faster. It just solved a big performance problem for me.

    This form of code is even neater. It uses the fact that you can use an array as a stack:

    var output = new Array();

    output.push("<table id='myTable1'><tbody id='myTbody'>");
    for (var j = 0; j < tableData.length; j++) {
    output.push("<tr class='tr" + (j%2) + "'>");
    output.push("<td class='col0'>" + tableData[j].alpha + "</td>");
    output.push("<td class='col1'>" + tableData[j].beta + "</td>");
    output.push("<td class='col2'>" + tableData[j].gamma + "</td>");
    output.push("<td class='col3'>" + tableData[j].delta + "</td>");
    output.push("<td class='col4'>" + tableData[j].epsilon + "</td></tr>");
    }
    output.push("</tbody></table>");
    document.getElementById("tableWrap").innerHTML = output.join("");

    • More tweaks
      2003-07-31 07:23:54  anonymous2 [View]

      That's great!
      I did not know that push() was so efficient.

      Do not forget that push() is not available in some older versions of IE resp. Javascript.

      Hubert
  • What about the document.write() method
    2003-05-20 16:31:12  anonymous2 [View]

    but I don't think it is more efficient.
    Sébastien.
    • What about the document.write() method
      2003-05-20 17:39:03  Danny Goodman | O'Reilly Author [View]

      You can't use document.write() to modify a portion of a document that has already loaded (unless you put that portion into its own iframe element). Even so, you'd be dealing with strings, which are not the most efficient data types to manipulate in JavaScript (you could pick up some performance via the array technique well-described in an earlier message).

      Danny
      http://www.dannyg.com
  • Tara McGoldrick Walsh photo Thanks for the feedback
    2003-05-12 12:30:25  Tara McGoldrick Walsh | [View]

    And stay tuned. We'll be adding an example in the next day or so. --Ed.
  • innerHTML tuned up
    2003-05-11 23:39:47  anonymous2 [View]

    In my experience it is the excessive use of the "+=" operator on strings which is using most of the time.
    So I suggest to use an array of strings as follows and concatenate at the latest possible moment using the join() method.

    var output = new Array();
    var i = 0;

    output[i++] = "<table id='myTable1'><tbody id='myTbody'>";
    for (var j = 0; j < tableData.length; j++) {
    output[i++] = "<tr class='tr" + (j%2) + "'>";
    output[i++] = "<td class='col0'>" + tableData[j].alpha + "</td>";
    output[i++] = "<td class='col1'>" + tableData[j].beta + "</td>";
    output[i++] = "<td class='col2'>" + tableData[j].gamma + "</td>";
    output[i++] = "<td class='col3'>" + tableData[j].delta + "</td>";
    output[i++] = "<td class='col4'>" + tableData[j].epsilon + "</td></tr>";
    }
    output[i++] = "</tbody></table>";
    document.getElementById("tableWrap").innerHTML = output.join("");

    Try it,
    Hubert

    hubert.kauker@web.de
  • Where is the working example?
    2003-05-10 13:25:45  anonymous2 [View]

    You forgot to include a working example.
  • where's the beef?
    2003-05-09 12:56:11  anonymous2 [View]

    Where's the working example?