DOM access is expensive; it’s the most common bottleneck when it comes to JavaScript performance.
The bottom line is that DOM access should be reduced to minimum. This means:
- Avoiding DOM access in loops
- Assigning DOM references to local variables and working with the locals
- Using selectors API where available
- Caching the length when iterating over HTML collections
=================
Consider the following example where the second (better) loop, despite being longer, will be tens to hundreds of times faster, depending on the browser:
var startTimeStamp = new Date().getTime(), startTimeStamp2, duration, duration2, content; for (var i = 0; i < 100000; i++) { document.getElementById('result').innerHTML = i; } duration = new Date().getTime() - startTimeStamp; // javascript benchmark - difference between start and end time in milliseconds document.getElementById('log').innerHTML = 'Operation with 100,000 DOM manipulation in the loop lasts ' + duration + ' milliseconds;'; startTimeStamp2 = new Date().getTime(); for (var j = 0; j < 100000; j++) { content = j; } document.getElementById('result2').innerHTML = content; duration2 = new Date().getTime() - startTimeStamp2; // javascript benchmark - difference between start and end time in milliseconds document.getElementById('log2').innerHTML = 'Operation with DOM manipulation outside the loop lasts ' + duration2 + ' milliseconds;';
=================
In the next snippet, the second example (using a local variable style) is better and faster:
// antipattern var padding = document.getElementById("result").style.padding, margin = document.getElementById("result").style.margin; // better and faster var style = document.getElementById("result").style, padding = style.padding, margin = style.margin;
=================
Using selector APIs means using the methods:
document.querySelector("ul .selected"); document.querySelectorAll("#widget .class");
These methods accept a CSS selector string and return a list of DOM nodes that match
the selection. The selector methods are available in modern browsers (and in IE since
version 8) and will always be faster than if you do the selection yourself using other
DOM methods.
=================
In for loops you iterate over arrays or array-like objects such as arguments and HTMLCollection objects. The usual for loop pattern looks like the following:
for ( var i = 0; i < dom_array.length; i++ ) { // do something with dom_array[i] }
A problem with this pattern is that the length of the array is accessed on every loop iteration. This can slow down your code, especially when dom_array is not an array but an HTMLCollection object.
The trouble with collections is that they are live queries against the underlying document (the HTML page). This means that every time you access any collection’s
length, you’re querying the live DOM, and DOM operations are expensive in general. That’s why a better pattern for for loops is to cache the length of the array (or collection)
you’re iterating over, as shown in the following example:
for ( var i = 0, max = dom_array.length; i < max; i++ ) { // do something with dom_array[i] }
This way you retrieve the value of length only once and use it during the whole loop. Caching the length when iterating over HTMLCollections is faster across all browsers.
======================
javascript DOM insert
Updating the DOM can cause the browser to repaint the screen and also often reflow (recalculate elements’ geometry), which can be expensive.
Again, the general rule of thumb is to have fewer DOM updates, which means batching changes and performing them outside of the 'live' document tree.
When you need to create a relatively big subtree, you should do so without adding to the live document until the end. For this purpose you can use a document fragment to contain all your nodes.
var startTimeStamp = new Date().getTime(), startTimeStamp2, duration, duration2, frag; for (var i = 0; i < 100000; i++) { document.getElementById('result').innerHTML = ''; // empty p = document.createElement('p'); t = document.createTextNode('paragraph'); p.appendChild(t); document.getElementById('result').appendChild(p); } duration = new Date().getTime() - startTimeStamp; // javascript benchmark - difference between start and end time in milliseconds document.getElementById('log').innerHTML = 'Operation with 100,000 DOM manipulation in the loop lasts ' + duration + ' milliseconds;'; startTimeStamp2 = new Date().getTime(); frag = document.createDocumentFragment(); for (var j = 0; j < 100000; j++) { document.getElementById('result2').innerHTML = ''; // empty p = document.createElement('p'); t = document.createTextNode('paragraph'); p.appendChild(t); frag.appendChild(p); } document.getElementById('result2').appendChild(frag); document.getElementById('result2').innerHTML = ''; // empty duration2 = new Date().getTime() - startTimeStamp2; // javascript benchmark - difference between start and end time in milliseconds document.getElementById('log2').innerHTML = 'Operation with DOM manipulation using offline fragment outside the loop lasts ' + duration2 + ' milliseconds;';
In this example the live document is updated only once, causing a single reflow/repaint, as opposed to one for every paragraph, as was the case in the previous antipattern snippet.
javascript DOM update
When you update an existing part of the tree, you can make a clone of the root of the subtree you’re about to change, make all the changes to the clone, and then when you’re done, swap the original with the clone:
var startTimeStamp = new Date().getTime(), startTimeStamp2, duration, duration2, frag; for (var i = 0; i < 100000; i++) { document.getElementById('result').innerHTML = ''; // empty p = document.createElement('p'); t = document.createTextNode('paragraph'); p.appendChild(t); document.getElementById('result').appendChild(p); } document.getElementById('result').innerHTML = ''; // empty duration = new Date().getTime() - startTimeStamp; // javascript benchmark - difference between start and end time in milliseconds document.getElementById('log').innerHTML = 'Operation with 100,000 DOM manipulation in the loop lasts ' + duration + ' milliseconds;'; var oldnode = document.getElementById('result2'), clone = oldnode.cloneNode(true); startTimeStamp2 = new Date().getTime(); for (var j = 0; j < 100000; j++) { clone.innerHTML = ''; // empty p = document.createElement('p'); t = document.createTextNode('paragraph'); p.appendChild(t); clone.appendChild(p); } oldnode.parentNode.replaceChild(clone, oldnode); document.getElementById('result2').innerHTML = ''; // empty duration2 = new Date().getTime() - startTimeStamp2; // javascript benchmark - difference between start and end time in milliseconds document.getElementById('log2').innerHTML = 'Operation with DOM manipulation using offline fragment outside the loop lasts ' + duration2 + ' milliseconds;';
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
- Creating Elements
- Inserting Elements Before & After
- Inserting Elements As Children
- Moving Elements
- Removing Elements
- Adding & Removing CSS Classes
- Adding/Removing/Changing Attributes
- Adding & Changing Text Content
- Adding/Updating Element Styles
- Micro-libraries For More Help
- Next
Creating Elements
jQuery
$('<div></div>');
DOM API
// IE 5.5+
document.createElement('div');
Wow, that was pretty easy. jQuery saved us a few keystrokes, but that's hardly
worth the bytes.
Inserting Elements Before & After
Let's create an element and insert it after another specific element.
So, we start with:
<div id="1"></div>
<div id="2"></div>
<div id="3"></div>
... and we'd like to create a new element with an ID of '1.1' and insert it
between the first two DIVs, giving us this:
<div id="1"></div>
<div id="1.1"></div>
<div id="2"></div>
<div id="3"></div>
jQuery
$('#1').after('<div id="1.1"></div>');
DOM API
// IE 4+
document.getElementById('1')
.insertAdjacentHTML('afterend', '<div id="1.1"></div>');
document.querySelector('#a2').insertAdjacentHTML('afterend', '<div id="2.1">2.1</div>');
Ha! Take THAT jQuery! Pretty easy in every browser just relying on the tools
built into the browser.
Ok, what if we want to insert a new element BEFORE the first div, giving us this:
<div id="0.9"></div>
<div id="1"></div>
<div id="2"></div>
<div id="3"></div>
jQuery
$('#1').before('<div id="0.9"></div>');
DOM API
// IE 4+
document.getElementById('1')
.insertAdjacentHTML('beforebegin', '<div id="0.9"></div>');
Pretty much the same as the last, with the exception of a different method call
for jQuery and a different parameter for the plain 'ole JavaScript approach.
Inserting Elements As Children
Let's say we have this:
<div id="parent">
<div id="oldChild"></div>
</div>
...and we want to create a new element and make it the first child of the parent,
like so:
<div id="parent">
<div id="newChild"></div>
<div id="oldChild"></div>
</div>
jQuery
$('#parent').prepend('<div id="newChild"></div>');
DOM API
// IE 4+
document.getElementById('parent')
.insertAdjacentHTML('afterbegin', '<div id="newChild"></div>');
...or create a new element and make it the last child of #parent:
<div id="parent">
<div id="oldChild"></div>
<div id="newChild"></div>
</div>
jQuery
$('#parent').append('<div id="newChild"></div>');
DOM API
// IE 4+
document.getElementById('parent')
.insertAdjacentHTML('beforeend', '<div id="newChild"></div>');
All of this looks a lot like the previous section that dealt with insertion of
new elements. Again, it's pretty simple to do all of this, cross-browser, without
any help from jQuery (or any other library).
Moving Elements
Consider the following markup:
<div id="parent">
<div id="c1"></div>
<div id="c2"></div>
<div id="c3"></div>
</div>
<div id="orphan"></div>
What if we want to relocate the #orphan as the last child of the #parent?
That would give us this:
<div id="parent">
<div id="c1"></div>
<div id="c2"></div>
<div id="c3"></div>
<div id="orphan"></div>
</div>
jQuery
$('#parent').append($('#orphan'));
DOM API
// IE 5.5+
document.getElementById('parent')
.appendChild(document.getElementById('orphan'));
Simple enough without jQuery. But what if we want to make #orphan the first child
of #parent, giving us this:
<div id="parent">
<div id="orphan"></div>
<div id="c1"></div>
<div id="c2"></div>
<div id="c3"></div>
</div>
jQuery
$('#parent').prepend($('#orphan'));
DOM API
// IE 5.5+
document.getElementById('parent')
.insertBefore(document.getElementById('orphan'), document.getElementById('c1'));
We can still complete this in one line, but it's a bit less intuitive & verbose
without jQuery. Still, not too bad.
Removing Elements
How can we remove an element from the DOM? Let's say we know an element
with an ID of 'foobar' exists. Let's kill it!
jQuery
$('#foobar').remove();
DOM API
// IE 5.5+
document.getElementById('foobar').parentNode
.removeChild(document.getElementById('foobar'));
The DOM API approach is certainly a bit verbose and ugly, but it works! Note
that we don't have to be directly aware of the parent element, which is nice.
Adding & Removing CSS Classes
We have a simple element:
<div id="foo"></div>
...let's add a CSS class of "bold" to this element, giving us:
<div id="foo" class="bold"></div>
jQuery
$('#foo').addClass('bold');
DOM API
document.getElementById('foo').className += 'bold';
Let's remove that class now:
jQuery
$('#foo').removeClass('bold');
DOM API
// IE 5.5+
document.getElementById('foo').className =
document.getElementById('foo').className.replace(/^bold$/, '');
As usual, more characters, but still easy without jQuery.
Adding/Removing/Changing Attributes
Let's start out with a simple element, like this:
<div id="foo"></div>
Now, let's say this <div>
actually functions as a button. We should attach
the approriate role
attribute to make this element more accessible.
jQuery
$('#foo').attr('role', 'button');
DOM API
// IE 5.5+
document.getElementById('foo').setAttribute('role', 'button');
In both cases, a new attribute can be created, or an existing attribute can
be updated using the same code.
What if the behavior of our <div>
changes, and it no longer functions as a
button. In fact, it's just a plain 'ole <div>
now with some trivial text or
markup. Let's remove that role
...
jQuery
$('#foo').removeAttr('role');
DOM API
// IE 5.5+
document.getElementById('foo').removeAttribute('role');
Adding & Changing Text Content
Now, we have the following markup:
<div id="foo">Hi there!</div>
...but we wan't to update the text to say "Goodbye!".
jQuery
$('#foo').text('Goodbye!');
Note that you can also easily retrieve the current text of the element by calling
text
with no parameters.
DOM API
// IE 5.5+
document.getElementById('foo').innerHTML = 'Goodbye!';
// IE 5.5+ but NOT Firefox
document.getElementById('foo').innerText = 'GoodBye!';
// IE 9+
document.getElementById('foo').textContent = 'Goodbye!';
Both properties above will return the current element's HTML/text as well.
The advantage to using innerText
or textContent
is that any HTML is escaped,
which is a great feature if the content is user-supplied and you only ever want
to include text as content for the selected element.
Adding/Updating Element Styles
Generally, adding styles inline or with JavaScript is a "code smell",
but it may be needed in some unique instances. For those cases, I'll show you
how that can be done with jQuery and the DOM API.
Given a simple element with some text:
<span id="note">Attention!</span>
...we'd like to make it stand out a bit more, so let's make it appear bold.
jQuery
$('#note').css('fontWeight', 'bold');
DOM API
// IE 5.5+
document.getElementById('note').style.fontWeight = 'bold';