Share via


DOM Performance Tips and Tricks

As of December 2011, this topic has been archived. As a result, it is no longer actively maintained. For more information, see Archived Content. For information, recommendations, and guidance regarding the current version of Internet Explorer, see Internet Explorer Developer Center.

Jacob Rossi
Program Manager, Internet Explorer
Microsoft Corporation

Published: July 2009

As Web pages and applications become richer, more data driven, and more dynamic, the need to improve performance becomes more important. One key area where performance gains can often be achieved is when accessing and manipulating the Document Object Model (DOM). While the DOM can be a very powerful tool, it can be a potential performance pitfall when not used correctly. This article addresses some common methods to ensure high performance use of the DOM. Engineering your code with performance in mind from the beginning is generally easier than reengineering poorly performing sites. However, many of the tips and tricks in this article can be implemented after the fact as point fixes.

This topic contains the following sections:

  • Understanding the DOM

  • Avoid Time-intensive API's

  • Use Smart Coding Patterns

    • Tip 1: Use IE8 Standards Mode
    • Tip 2: Minimize DOM Access
    • Tip 3: Cache Objects When Possible
    • Tip 4: Use the Selector API
    • Tip 5: Use Event Delegation
  • Finding DOM Intensive Code

Understanding the DOM

Basic knowledge of how the DOM works in Internet Explorer is helpful for understanding the origins of some of the performance gains mentioned in this article. The DOM is initially constructed as the web page is parsed. However, it is constantly adjusted and updated as the web page is manipulated by either user, programmatic, or browser interaction. For those unfamiliar with the structure and capabilities of the Internet Explorer DOM, About the W3C Document Object Model is an excellent starting point.

An important aspect of the DOM is described in Figure 1. Simply put, DOM access is much more expensive than most other JavaScript. This is partly due to the immense nature of the DOM and partly due to the fact that DOM access requires the use of an internal interface layer because it is not a native part of the JavaScript language itself. Nevertheless, that is not to say that DOM access should be avoided altogether. Rather, minimizing unnecessary DOM interactions can often yield performance gains.

DOM Performance Costs

Figure 1: DOM Performance Costs

Avoid Time-intensive APIs

For convenience, the DOM offers many shortcut APIs that get the job done in a simple way. Nevertheless, sometimes alternate APIs or coding patterns can be utilized to produce the same result but at a lesser cost. When this article was first written, the following APIs accounted for a large percentage of the total DOM API run-time on some of the top sites by traffic.

document.all

Suggestion: Use document.getElementById

The document.getElementById function performs up to three times faster than document.all. Available in Internet Explorer 5 and later, document.getElementById is more interoperable with other major Web browser; document.all is deprecated.

src

Suggestion: Use CSS to change images.

This API is often used to change images dynamically. There are some times when CSS can do this faster. For rollover effects, use the :hover pseudoclass.

For more complicated scenarios, consider using CSS image sprites. The performance gains from this strategy can vary, though. This strategy works best mostly for small images being replaced with high frequency. The benefits of this technique are more responsive image changes and less DOM usage. However, the cost of this technique is a greater initial load time (the entire image sprite is downloaded at load time). You might find for some scenarios that src works better.

Suggestion: Server-side sessions

Minimize the use of cookies on the user's computers. Most server technologies (PHP, ASP.NET, and so on) allow user data to be saved on the server while saving a single Session ID cookie to the user's computer. Avoiding expensive calls that require cookie lookup might improve performance.

innerHTML, insertAdjacentHTML, write, writeLn

Suggestion: Minimize/batch usage, compare performance with createElement/insertAdjacentElement

Calling these APIs requires the string be parsed, inserted into the DOM, applicable styles applied, layout redone, and finally rendered. All of this adds up to a high performance cost. Care should be made that blocks of HTML be inserted at once rather than multiple times (see Tip 2). In many cases using createElement followed by insertAdjacentElement (or appendChild) can be faster.

getElementById

Suggestion: Cache object references.

Write your functions to take in object references as opposed to IDs that have to then be looked up.

The getElementById API can become a bottleneck for large DOM Web pages. Caching object references avoids unnecessary lookups in the DOM. Functions should be written to take in pointers to objects rather than element IDs that need to then be looked up. For sites where getElementById is still a bottleneck, consider simplifying your page's DOM tree structure. Avoid coding patterns like nested tables to reduce the complexity of traversing the DOM. See Tip 3 for more information about caching object references.

appendChild

Suggestion: When building a subtree that will affect layout (such as a table), wait to attach the root node to the document until the entire tree has been built. Build trees from the top down.

While appendChild is often used to get performance gains in building tables (compared to insertCell or insertRow), it's best to build the subtree outside of the main tree and then append it once built. In other words, wait to append the root node of the subtree to the document until the entire subtree has been built. This avoids unnecessary intermediate layout updates for each element appendage. Furthermore, assembling the subtree top-down avoids additional markup and splicing operations as well as increasing performance.

The following code example demonstrates a slow technique.

var res = document.getElementById("testarea");
var t = document.createElement("table");
var tbl = res.appendChild(t); 
for(var i=0;i<10000;i++) {
  var row = document.createElement("tr");
  var cell = document.createElement("td");
  var txt = document.createTextNode("Cell " + i);
  tbl.appendChild(row);
  row.appendChild(cell);
  cell.appendChild(txt);
}

The following code example is a faster version of the earlier one.

var res = document.getElementById("testarea");
var t = document.createElement("table");
for(var i=0;i<10000;i++) {
  var row = document.createElement("tr");
  var cell = document.createElement("td");
  var txt = document.createTextNode("Cell " + i);
  t.appendChild(row);	
  row.appendChild(cell);
  cell.appendChild(txt);	
}
res.appendChild(t);

The slow example assembles the subtree directly in the main tree. The fast example assembles the tree separately and then appends it to the document. Note that both examples make sure to assemble the tree top-down. An example of assembling it bottom-up would be if the cell had been appended to the row and then the row appended to the table.

style, display, visibility

Suggestion: Avoid repeatedly modifying the style in the DOM.

Include your dynamic styles as separate classes in CSS. Then use the DOM to simply switch the class of the element. See Tip 4.

Allow CSS to select the elements for you. Predefined classes avoid having to be computed on the fly during runtime. Also, elements with the visibility:hidden attribute still affect layout while those with display:none do not.

getElementsByTagName

Suggestion: If you plan on filtering the results of this API, consider using querySelectorAll to more specifically identify the elements you're looking for.

Avoid having getElementsByTagName return too much detail. Very often the matched elements of this function are then filtered for the real elements you want. Giving desired elements a class or ID can allow you to use querySelectorAll to return a much more precisely matched set of elements. Note that the querySelectorAll API is new to IE8 and is only avilable in IE8 standards mode. It may be necessary, depending on your audience, to provide a fallback for users with earlier versions of Internet Explorer.

length

Suggestion: Reduce usage and cache when possible.

In most cases, when the length of a collection needs to be used repeatedly it is either a static value or the quantity by which it is changing is known. Therefore, it is unnecessary to call this property over and over. Cache the value in a local variable. Increment/decrement it locally if your code modifies the length of the collection. Tip 3.

Suggestion: Use feature/object detection to improve forward compatibility.

New versions of Internet Explorer often increase performance by adding or changing functionality. The best way to ensure your code stays forward compatible is to avoid using browser detection as a means to deciding feature/API availability. Testing to see if the object exists is a much better method in most scenarios. For more information, see Detecting Internet Explorer More Effectively.

Use Smart Coding Patterns

Follow these five tips to help improve the performance of Web pages that manipulate the DOM.

Tip 1: Use IE8 Standards Mode

Internet Explorer 8 offers new standards support as well as a better DOM. Ensuring your site is compatible with IE8 Standards mode will provide these benefits and will make it easier to ensure longevity of your site's compatibility with modern browsers. IE8 Standards mode will also allow your code to take advantage of new APIs, like querySelectorAll. To be sure your site renders in IE8 Standards mode, include the following meta tag in your document's HEAD section.

<meta http-equiv="X-UA-Compatible" content="IE=8" />

In addition, add a standards-compliant directive to ensure that earlier versions render your pages as richly as possible.

Tip 2: Minimize DOM Access

Many DOM interactions require the browser to reevaluate the layout of the page. Because layout events take time, organizing multiple changes to happen all at once can improve performance. One particularly common way of causing multiple unnecessary layout events is when using the += operator when modifying layout properties. Consider the following example.

document.myDIVid.width += 20;
document.myDIVid.height += 20;

The += operator in this case requires the current width be accessed from the DOM, the value increased by 20, then the new value updated in the DOM which triggers a layout event. The whole process is then repeated for adjusting the height. The reason two layout events are caused is that in some cases adjusting the width could affect the height. So the layout engine needs to recalculate the height in order to use += on the height. However, in most cases this intermediate layout event is unneeded. A better method is the following:

var h = document.myDIVid.height;
document.myDIVid.width += 20;
document.myDIVid.height = h + 20;

Also note that a locally scoped variable was used. Scoping your variables as narrowly as possible is a good performance-enhancing practice in JavaScript (see Figure 1).

Another common way to cause unnecessary DOM access and layout events is to append HTML or text inside an element within a loop. Consider the following example.

for(var i=0;i<50;i++) {
	document.myListBox.innerHTML += "<li>Item " + i;
}

Instead, consider building up a string and then inserting the HTML all at once.

var listHTML = "<li>Item 0</li>";
for(var i=1;i<50;i++) {
  listHTML.push("<li>Item " + i + "</li>");
}
document.myListBox.innerHTML = listHTML;

Minimizing DOM access and layout events will help improve the performance of your code. Moreover, these optimizations can often be done without changing the overall functionality of your code.

Tip 3: Cache Objects When Possible

A corollary to reducing unintentional layout events is to also avoid unnecessary DOM access. A good rule of thumb is to avoid reading a property or executing a method that will reproduce an already captured result. Where this strategy often comes into play is iteration. Consider the following example.

for(var i=0; i<50; i++) {
   arr[i]=document.getElementById('myListBox').options[i].value;
}

This code block traverses the DOM for the element with the ID 'myListBox' every iteration (50 times). However, since the code is synchronous and the list box's content is not changing, the element can be cached locally to avoid these unnecessary DOM lookups. A better method is the following:

var mybox = document.getElementById('myListBox');
for(var i=0; i<50; i++) {
  arr[i] = mybox.options[i].value;
}

Another time to apply this concept is when reading properties that are not changing (or the quantity by which they are changing is known by the code). Consider this example.

function growBox(id) {
  var elm = document.getElementById(id);
  var w = parseInt(elm.style.width)+20;
  if (w<500) {
    elm.style.width = w + "px";
    setTimeout(function () { growBox(id); },100);
  }
}

This simple method to grow a box takes care to pull elements out of the DOM, modify them locally, and then put them back in the DOM. However, if we know that the width of the element is not getting modified anywhere else, we can remove the unnecessary width reads. Furthermore, it is unnecessary to look up the element reference in the DOM in every call. Here's a better example:

function doGrow(elm,w) {
  w += 20;
  if (w<500) {
    elm.style.width = w + "px";
    setTimeout(function () { doGrow(elm,w); },100);
  }
}

function growBox(id) {
  var elm = document.getElementById(id);
  var w = parseInt(elm.style.width);
  doGrow(elm,w);
}

With the use of the wrapper function, we only have to look up the element reference and style property once.

Tip 4: Use the Selector API

Many times JavaScript is used to modify element style on the page. However, if the dynamic style could be applied in CSS, leave it to CSS to do the element finding. Consider the DOM traversal method:

while(element.nextSibiling) {
  element = element.nextSibling;
element.style.backgroundColor = "yellow";
}

This code essentially is making the DOM do the work of a CSS selector. It may seem painfully obvious now, but this is a fairly common code pattern on the Web. A better solution is to define a CSS class that selects these elements. The class can then be added to the document body at runtime.

var newClass = "dynamicClass";
var docbody = document.body;
docbody.className = docbody.className ? docbody.className + newClass : newClass;

An added benefit to this technique is that both the original style and the dynamic style are defined in the document's CSS instead of in script. Removing the specific style attributes from script allows a designer to simply drop in a new CSS file that changes both the dynamic and static appearances. Don't forget about pseudo-selectors like :hover that can be used instead of event listeners for simple style behaviors. Again, note that new CSS 2.1 selectors were added to Internet Explorer 8. Thus, if you plan support users with older versions of Internet Explorer, you might need to supply a fallback.

If you wish to do more than just change the style of an element, you can use this same strategy with the querySelectorAll function to return object references. This API was also introduced in Internet Explorer 8, so you'll need to provide a fallback if you want to support previous versions of Internet Explorer.

Tip 5: use Event Delegation

For scenarios that involve similar events being fired for a large number of child elements, a technique called event delegation can be employed. For example, if a table has many rows and you wish to listen to mouseclick events for each row, attaching an event listener to each row is not very efficient. Instead, attach a single event listener to a parent element (such as the table) and have it dispatch the appropriate actions based on the event target. Consider the following inefficient example.

<table id="myTable">
 <tbody>
  <tr id="row0" onclick="alert('row0');"><td><p>Some text.</p></td></tr>
  <tr id="row1" onclick="alert('row1');"><td><p>More text.</p></td></tr>
  <tr id="row2" onclick="alert('row2');"><td><p>Even more.</p></td></tr>
  ...
  <tr id="row999" onclick="alert('row999');"><td><p>A ton.</p></td></tr>
 </tbody>
</table>

While the concept is exaggerated in this example, the point is that repeated event handlers are excessive. Instead, use this example.

var elm = document.getElementById("myTable");
elm.attachEvent("onlick",function() {
  var e = window.event;
  alert(e.srcElement);
});

Events on child elements (both statically and dynamically created) will bubble up and be handled by the event delegator. This benefit removes the additional effort of having to also attach new event listeners to dynamically inserted children. The event delegation concept does not only apply to scenarios where the action produced is common to each element. Rather, it can be extended to make general event dispatching functions. Consider this incomplete example of an event dispatcher:

var dispatcher = function() {
  var e = window.event;
  switch(e.srcElement.id) {
    case "row1":
      doHeaderRowAction(e);
      break;
    case "row999":
      doFooterRowAction(e);
      break;
  default:
    doGeneralRowAction(e);
  }
}

It should be noted that for large switch/case statements there can be performance increases for using a table lookup of function pointers instead.

Caution: The concept of event dispatching relies on event bubbling (the process by which an event gets fired on elements up the parent chain). Not all events bubble. Notable examples include change, focus, blur, reset, abort, submit, select, scroll, resize, load, and unload. However, Internet Explorer has the focusin and focusout events which are similar to focus and blur except that they bubble.

Finding DOM Intensive Code

While the optimal method is to engineer with performance in mind from the start, this method is often not the realistic scenario. Optimizing existing code for performance can be difficult. With Internet Explorer 8, the Developer Tools can be useful to target slow functions. One simple, but effective, algorithm is to use the JavaScript profiler tool:

  1. Open the Developer Tools by pressing F12 or selecting it from the Tools menu.

    The Developer Tools Profiler

    Figure 2: The Developer Tools Profiler

  2. On the Profiler tab, click Start Profiling.

  3. Perform your page interaction (reload, user interaction, etc.) which executes the script.

  4. Click Stop Profiling.

  5. Sort by Exclusive Time (default).

  6. Analyze the worst performing methods for coding paradigms mentioned above. Apply fixes where applicable.

For more information see Profiling Script with the Developer Tools.

When analyzing for DOM performance, it's also important to consider scalability of your code. Performance-poor DOM interaction tends to show up better when considering large scale operations. For example, if your JavaScript fetches data via an AJAX request and inserts it into a table, try running it where it fetches 10 or even 100 times as many rows (even if this is an unrealistic scenario). Many of the cited examples of poor performing DOM interactions will be accentuated when testing at a larger scale.