Efficient JavaScript
DOM
In general, there are three main things that can cause DOM to perform slowly. The first is when a script performs some extensive DOM manipulation, such as building a new tree from some retrieved data. The second is when a script triggers too many reflows or repaints. The third is when a script takes a slow approach to locating a desired node in the DOM tree.
The second and third are the most common, and the most significant, so these will be dealt with first.
Repaint and reflow
Repaint - also known as redraw - is what happens whenever something is made visible when it was not previously visible, or vice versa, without altering the layout of the document. An example would be when adding an outline to an element, changing the background color, or changing the visibility style. Repaint is expensive in terms of performance, as it requires the engine to search through all elements to determine what is visible, and what should be displayed.
A reflow is a more significant change. This will happen whenever the DOM tree is manipulated, whenever a style is changed that affects the layout, whenever the className property of an element is changed, or whenever the browser window size is changed. The engine must then reflow the relevant element to work out where the various parts of it should now be displayed. Its children will also be reflowed to take the new layout of their parent into account. Elements that appear after the element in the DOM will also be reflowed to calculate their new layout, as they may have been moved by the initial reflows. Ancestor elements will also reflow, to account for the changes in size of their children. Finally, everything is repainted.
Reflows are very expensive in terms of performance, and is one of the main causes of slow DOM scripts, especially on devices with low processing power, such as phones. In many cases, they are equivalent to laying out the entire page again.
Keeping the number of reflows to a minimum
There are many times that a script will need to do something that will trigger a repaint or reflow. Animations are built on reflows, and these will continue to be desired. So reflows are a fact of Web development, and to keep scripts running fast, they should be kept to a minimum while still having the same overall effect.
Browsers may choose to wait until the end of a script thread before reflowing to show the changes. Opera will wait until enough changes have been made, enough time has elapsed, or the end of the thread is reached. This means that if the changes happen quickly enough in the same thread, they may only produce one reflow. However, this cannot be relied on, especially considering the various different speeds of devices that Opera runs on.
Note that some elements have significantly slower reflows than others. Reflowing an element with table display, can take as much as three times as long as reflowing an equivalent element with block display.
Minimal reflow
Normal reflows may affect the whole document. The more of the document that is reflowed, the longer the reflow will take. Elements that are positioned absolutely or fixed, do not affect the layout of the main document, so if they reflow, they are the only thing that reflows. The document behind them will need to repaint to allow for any changes, but this is much less of a problem than an entire reflow.
So is an animation does not need to be applied to the whole document, it is better if it can be applied only to a positioned element. For most animations, this is all that is needed anyway.
Document tree modification
Document tree modification will trigger reflow. Adding new elements to the DOM, changing the value of text nodes, or changing various attributes will all be enough to cause a reflow. Making several changes one after the other, may trigger more than one reflow, so in general, it is best to make multiple changes in a non-displayed DOM tree fragment. The changes can then be made to the live document's DOM in one single operation:
var docFragm = document.createDocumentFragment();
var elem, contents;
for( var i = 0; i < textlist.length; i++ ) {
elem = document.createElement('p');
contents = document.createTextNode(textlist);
elem.appendChild(contents);
docFragm.appendChild(elem);
}
document.body.appendChild(docFragm);
Document tree modification can also be done on a clone of the element, which is then swapped with the real element after the changes are complete, resulting in a single reflow. Note that this approach should not be used if the element contains any form controls, as any changes the user makes to their values, are not reflected in the main DOM tree. It should also not be done if you need to rely on event handlers being attached to the element or its children, since in theory they should not be cloned.
var original = document.getElementById('container');
var cloned = original.cloneNode(true);
cloned.setAttribute('width','50%');
var elem, contents;
for( var i = 0; i < textlist.length; i++ ) {
elem = document.createElement('p');
contents = document.createTextNode(textlist);
elem.appendChild(contents);
cloned.appendChild(elem);
}
original.parentNode.replaceChild(cloned,original);
Modifying an invisible element
When an element has its display style set to none, it will not need to repaint, even if its contents are changed, since it is not being displayed. This can be used as an advantage. If several changes need to be made to an element or its contents, and it is not possible to combine these changes into a single repaint, the element can be set to display:none, the changes can be made, then the element can be set back to its normal display.
This will trigger two extra reflows, once when the element is hidden, and once when it is made to appear again, but the overall effect can be much faster. It may also cause unwanted jumping of the scrollbar, if the element itself affects the scrolling offset. However, it can easily be applied to a positioned element without causing an unsightly effect.
var posElem = document.getElementById('animation');
posElem.style.display = 'none';
posElem.appendChild(newNodes);
posElem.style.width = '10em';
... other changes ...
posElem.style.display = 'block';
Taking measurements
As stated earlier, the browser may cache several changes for you, and reflow only once when those changes have all been made. However, note that taking measurements of the element will force it to reflow, so that the measurements will be correct. The changes may or may not not be visibly repainted, but the reflow itself still has to happen behind the scenes.
This effect is created when measurements are taken using properties like offsetWidth, or using methods like getComputedStyle. Even if the numbers are not used, simply using either of these while the browser is still caching changes, will be enough to trigger the hidden reflow. If these measurements are taken repeatedly, you should consider taking them just once, and storing the result, which can then be used later.
var posElem = document.getElementById('animation');
var calcWidth = posElem.offsetWidth;
posElem.style.fontSize = ( calcWidth / 10 ) + 'px';
posElem.firstChild.style.marginLeft = ( calcWidth / 20 ) + 'px';
posElem.style.left = ( ( -1 * calcWidth ) / 2 ) + 'px';
... other changes ...
Making several style changes at once
Just like with DOM tree modifications, it is possible to make several style related changes at the same time, in order to minimise the number of repaints or reflows. The common approach is setting of styles one at a time:
var toChange = document.getElementById('mainelement');
toChange.style.background = '#333';
toChange.style.color = '#fff';
toChange.style.border = '1px solid #00f';
That approach could mean multiple reflows and repaints. There are two main ways to do this better. If the element itself needs to adopt several styles, whose values are all known in advance, the class of the element can be changed. It will then take on all the new styles defined for that class:
div {
background: #ddd;
color: #000;
border: 1px solid #000;
}
div.highlight {
background: #333;
color: #fff;
border: 1px solid #00f;
}
...
document.getElementById('mainelement').className = 'highlight';
The second approach is to define a new style attribute for the element, instead of assigning styles one by one. Most often this is suited to dynamic chages such as animations, where the new styles cannot be known in advance. This is done using either the cssText property of the style object, or by using setAttribute. Internet Explorer does not allow the second version, and needs the first. Some older browsers, including Opera 8, need the second approach, and do not understand the first. So the easy way is to check if the first version is supported and use that, then fall back to the second if not.
var posElem = document.getElementById('animation');
var newStyle = 'background: ' + newBack + ';' +
'color: ' + newColor + ';' +
'border: ' + newBorder + ';';
if( typeof( posElem.style.cssText ) != 'undefined' ) {
posElem.style.cssText = newStyle;
} else {
posElem.setAttribute('style',newStyle);
}
Trading smoothness for speed
As a developer, it is tempting to make an animation run as smoothly as possible, by using short timeouts, and small changes. For example, animated motion could be done using a 10ms interval, that moves an element 1 pixel at a time. An animation running that fast may work nicely on some PCs or some browsers. However, a 10ms interval is about the smallest that a browser can achieve without using 100% of most desktop CPUs. Some browsers will not even be able to manage that - requesting 100 reflows per second is quite a lot for most browsers. Lower powered computers, or device browsers, will not be able to perform at that speed, and the animation will feel slow and unresponsive.
It can be neccessary to swallow the developer pride, and trade some of the smoothness of the animation for speed instead. Changing the interval to 50ms, and the animation step to 5 pixels, will need much less processing power, and can make the animation run much faster on lower powered processors.
Avoid inspecting large numbers of nodes
When attempting to locate a specific node, or specific subset of nodes, use the inbuilt methods and collections of the DOM to narrow the search to as small a number of nodes as possible. For example, if you want to locate an unknown element in the document, that has a certain attribute, you could use this:
var allElements = document.getElementsByTagName('*');
for( var i = 0; i < allElements.length; i++ ) {
if( allElements.hasAttribute('someattr') ) {
...
}
}
Even if we ignore more advanced techniques such as XPath, that example still has two problems that make it slow. Firstly, it searches for every element, without attempting to narrow the search at all. Secondly, it still continues searching, even after it has found the element it wanted. Say for example, that the unknown element is known to be inside a div with the id inhere, this code could perform far better:
var allElements = document.getElementById('inhere').getElementsByTagName('*');
for( var i = 0; i < allElements.length; i++ ) {
if( allElements.hasAttribute('someattr') ) {
...
break;
}
}
If the unknown element is known to be a direct child of the div, this approach may be even faster, depending on the number of descendent elements of the div, compared with the length of its childNodes collection:
var allChildren = document.getElementById('inhere').childNodes;
for( var i = 0; i < allChildren.length; i++ ) {
if( allChildren.nodeType == 1 && allChildren.hasAttribute('someattr') ) {
...
break;
}
}
The basic intention is to avoid manually stepping through the DOM as much as possible. The DOM has many alternatives that may perform better in various circumstances, such as DOM 2 Traversal TreeWalker, instead of recursively stepping through childNodes collections.
Improve speed with XPath
A simple example would be building a table of contents in a HTML document, based on the H2-H4 elements. In HTML, these elements can appear in a variety of places, without any proper hierarchy, so a recursive function cannot be used to retrieve the elements in the correct order. Traditional DOM would use an approach like this:
var allElements = document.getElementsByTagName('*');
for( var i = 0; i < allElements.length; i++ ) {
if( allElements.tagName.match(/^h[2-4]$/i) ) {
...
}
}
In a document that contains perhaps 2000 elements, this can cause a significant delay, as each must be examined separately. XPath, when natively supported, offers a much faster approach, as the XPath querying engine can be optimised much more effectively than interpreted JavaScript. In some cases, it can be as much as two orders of magnitude faster. This example is equivalent to the traditional example, but uses XPath for improved speed.
var headings = document.evaluate( '//h2|//h3|//h4', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null );
var oneheading;
while( oneheading = headings.iterateNext() ) {
...
}
This version combines both; using XPath where possible, and falling back to traditional DOM if not:
if( document.evaluate ) {
var headings = document.evaluate( '//h2|//h3|//h4', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null );
var oneheading;
while( oneheading = headings.iterateNext() ) {
...
}
} else {
var allElements = document.getElementsByTagName('*');
for( var i = 0; i < allElements.length; i++ ) {
if( allElements.tagName.match(/^h[2-4]$/i) ) {
...
}
}
}
Avoid modifications while traversing the DOM
Certain types of DOM collections are live, in that if the relevant elements change while your script is looking at the collection, the collection will change without waiting for your script to finish first. This includes the childNodes collection, and the node list returned by getElementsByTagName.
If your script is looping through a collection like these, and at the same time, it adds elements to it, then you risk running into an infinite loop where you continually add entries into the collection before you reach the end of it. However, this is not the only problem. These collections can be optimised for performance. They can remember the length, and the last index within it that your script referenced, so that when you increment the index, they can quickly reference the next node.
If you modify any part of the DOM tree, even if it is not included in that collection, the collection must be reassessed to look for new entries. In doing so, it cannot remember the last index or length, as these may have changed, and the optimisation is lost:
var allPara = document.getElementsByTagName('p');
for( var i = 0; i < allPara.length; i++ ) {
allPara.appendChild(document.createTextNode(i));
}
This equivalent code performs around ten times faster in Opera, and some other current browsers such as Internet Explorer. It works by first building a static list of elements to modify, then performs the modifications while stepping through the static list instead of the node list returned by getElementsByTagName:
var allPara = document.getElementsByTagName('p');
var collectTemp = [];
for( var i = 0; i < allPara.length; i++ ) {
collectTemp[collectTemp.length] = allPara;
}
for( i = 0; i < collectTemp.length; i++ ) {
collectTemp.appendChild(document.createTextNode(i));
}
collectTemp = null;
Cache DOM values in script variables
Some values returned by DOM cannot be cached, and will be reasessed each time they are called. An example is the getElementById method. The following is an example of wasteful code:
document.getElementById('test').property1 = 'value1';
document.getElementById('test').property2 = 'value2';
document.getElementById('test').property3 = 'value3';
document.getElementById('test').property4 = 'value4';
That code makes four requests to locate the same object. The following code makes one request then stores it, meaning that for a single request, the speed is about the same, or very slightly slower while performing the assignment. However, each subsequent time the cached value is used, the command runs between five and ten times as fast in most current browsers, as the equivalent command in the example above:
var sample = document.getElementById('test');
sample.property1 = 'value1';
sample.property2 = 'value2';
sample.property3 = 'value3';
sample.property4 = 'value4';