Web DevCenter    
 Published on Web DevCenter (http://www.oreillynet.com/javascript/)
 See this if you're having trouble printing code examples


PC Hardware in  Nutshell, 2nd Edition

Dynamic HTML Tables: Improving Performance

by Danny Goodman, author of JavaScript & DHTML Cookbook 05/06/2003

Sorting an HTML table's data instantly via a click on a column header has been possible ever since browsers allowed arbitrary page-content modification. With the wide browser adoption of the W3C Document Object Model (DOM) and other de facto standards, a developer has so many ways to re-populate a table, it may be hard to choose "the best" approach. I've recently investigated this issue in search of the most efficient techniques. Some of the findings took me by surprise.

Tables occupying one or two screens of page space perform acceptably well, regardless of the approach you use. But when a table might contain hundreds of rows (and you must display the entire table at once), it's important to squeeze every ounce of performance out of the table-modification code. The lessons learned from this exercise may shape the way you code all of your replaceable content.

Design Assumptions

The focus of my investigation was on the table content assembly and rendering processes -- how long it takes to populate a table's tbody section with dynamically generated content. The test scenario consisted of a partially prefabricated table structure with a fixed table header row and a modifiable tbody element where dynamic content would appear. The skeletal structure of the hard-wired HTML portion follows:

<div class="tableWrapper">
<table id="myTable">
   <thead>
     <tr>
        <th>...</th>

     </tr>
   </thead>

   <tbody id="myTbody">
   </tbody>
</table>
</div>

Dynamically generated test tables consisted of 500 rows, each row displaying five columns of data. Each td element was to have a class attribute assigned to it signifying its column position (col0 through col4), while tr element class names (tr0 and tr1) alternated between rows. Stylesheet assignments were kept simple:

<style type="text/css">
table {width:80%}
.tableWrapper {text-align:center}
.tr0 {background-color:#ffffcc}
.tr1 {background-color:#ccffcc}
.col0 {width:5%}
.col1 {width:50%}
.col2 {width:10%}
.col3 {width:25%}
.col4 {width:10%}
</style>

Raw data for the table was delivered to the browser as an embedded JavaScript array of objects. Each entry (object) in the array corresponded to one row of the table; values of each of the five object properties (named alpha through epsilon) corresponded to one table cell's data. Scripts created the array and objects while the page loaded so that acquisition and delivery of the data to the client was not a part of the performance measurement. Data sorting prior to content assembly was also intentionally kept out of the measurements. Both of my current O'Reilly books contain ample examples of array sorting that can be applied to sorting table contents.

Content Assembly

Related Reading

JavaScript & DHTML Cookbook
Solutions and Example for Web Programmers
By Danny Goodman

Next, I had to decide on the range of techniques to test in my goal of populating the tbody element with newly created rows and cells. From the browser compatibility standpoint, I should point out that modifying a segment of an existing table element is sadly broken in Internet Explorer for the Macintosh. I'll have more to say about a cross-browser workaround later in this article. Therefore, all of the following discussion applies to other mainstream, W3C DOM-capable browsers, such as Internet Explorer 6 for Windows, Mozilla-based browsers, Opera 7, and Apple's Safari (still in beta release as of this writing).

While scripters have several ways of accumulating table content, I focused on three broad categories:

Within these three major categories are numerous subcategories. The one I was particularly interested in was comparing the performance of creating table-cell content as DOM text nodes versus string innerHTML values of td element objects.

Approach I: Table-specific Methods

First introduced in Internet Explorer 4 for Windows, the tableElement.insertRow() and tableRowElement.insertCell() methods provide direct access to document-node tree elements to which you wish to add rows and cells. The methods are convenient because they automatically create new DOM element objects for you. The methods return references to the newly created rows or cells, making it easy for your scripts to assign property values to those new, but otherwise unpopulated, elements. Filling the content of each cell via its innerHTML property results in a fairly compact source code sequence inside of a repeat loop that draws data from an array of objects:

var tbodyElem = document.getElementById("myTbody");
var trElem, tdElem;
// loop through 500-item tableData array
for (var j = 0; j < tableData.length; j++) {
   trElem = tbodyElem.insertRow(tbodyElem.rows.length);
   trElem.className = "tr" + (j%2);
   
   // first column
   tdElem = trElem.insertCell(trElem.cells.length);
   tdElem.className = "col0";
   tdElem.innerHTML = tableData[j].alpha;
   ...
   // last column
   tdElem = trElem.insertCell(trElem.cells.length);
   tdElem.className = "col4";
   tdElem.innerHTML = tableData[j].epsilon;
}

If you prefer to stay within the bounds of pure W3C DOM compatibility, the innerHTML property would be off-limits. Instead, use the text-node creation facilities of the W3C DOM, which bulks up the code just a bit (changes shown in bold):

var tbodyElem = document.getElementById("myTbody");
var trElem, tdElem, txtNode;
for (var j = 0; j < tableData.length; j++) {
   trElem = tbodyElem.insertRow(tbodyElem.rows.length);
   trElem.className = "tr" + (j%2);
   
   tdElem = trElem.insertCell(trElem.cells.length);
   tdElem.className = "col0";
   txtNode = document.createTextNode(tableData[j].alpha);
   tdElem.appendChild(txtNode);
   ...
   tdElem = trElem.insertCell(trElem.cells.length);
   tdElem.className = "col4";
   txtNode = document.createTextNode(tableData[j].epsilon);
   tdElem.appendChild(txtNode);
}

Comparing the performance of the innerHTML and text-node creation techniques produced mixed results. IE/Windows and Safari were marginally faster using innerHTML, while Mozilla-based browsers (on both Windows and Mac test platforms) showed inverse proportions in favor of text node creation, despite the increase in script statements that execute in the process.

More significant, however, is that on the same computer, IE 6 took three to four times longer to perform the task on all 500 rows than Netscape 7 did. Opera 7 in Windows was unbearably slow. Only Safari demonstrated a performance preference for direct DOM table modification methods over the other approaches studied, accomplishing its complete task in one-third to one-fifth the time required by Mozilla 1.2 on the same Macintosh system.

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.

Copyright © 2009 O'Reilly Media, Inc.