When innerHTML isn’t Fast Enough

This post isn't about the pros and cons of innerHTML vs. W3C DOM methods. That has been hashed and rehashed elsewhere. Instead, I'll show how you can combine the use of innerHTML and DOM methods to make your code potentially hundreds of times faster than innerHTML on its own, when working with large numbers of elements.

In some browsers (most notably, Firefox), although innerHTML is generally much faster than DOM methods, it spends a disproportionate amount of time clearing out existing elements vs. creating new ones. Knowing this, we can combine the speed of destroying elements by removing their parent using the standard DOM methods with creating new elements using innerHTML. (This technique is something I discovered during the development of RegexPal, and is one of its two main performance optimizations. The other is one-shot markup generation for match highlighting, which avoids needing to loop over matches or reference them individually.)

The code:

function replaceHtml(el, html) {
	var oldEl = typeof el === "string" ? document.getElementById(el) : el;
	/*@cc_on // Pure innerHTML is slightly faster in IE
		oldEl.innerHTML = html;
		return oldEl;
	@*/
	var newEl = oldEl.cloneNode(false);
	newEl.innerHTML = html;
	oldEl.parentNode.replaceChild(newEl, oldEl);
	/* Since we just removed the old element from the DOM, return a reference
	to the new element, which can be used to restore variable references. */
	return newEl;
};

You can use the above as el = replaceHtml(el, newHtml) instead of el.innerHTML = newHtml.

innerHTML is already pretty fast...is this really warranted?

That depends on how many elements you're overwriting. In RegexPal, every keydown event potentially triggers the destruction and creation of thousands of elements (in order to make the syntax and match highlighting work). In such cases, the above approach has enormous positive impact. Even something as simple as el.innerHTML += str or el.innerHTML = "" could be a performance disaster if the element you're updating happens to have a few thousand children.

I've created a page which allows you to easily test the performance difference of innerHTML and my replaceHtml function with various numbers of elements. Make sure to try it out in a few browsers for comparison. Following are a couple examples of typical results from Firefox 2.0.0.6 on my system:

1000 elements...
innerHTML (destroy only): 156ms
innerHTML (create only): 15ms
innerHTML (destroy & create): 172ms
replaceHtml (destroy only): 0ms (faster)
replaceHtml (create only): 15ms (~ same speed)
replaceHtml (destroy & create): 15ms (11.5x faster)

15000 elements...
innerHTML (destroy only): 14703ms
innerHTML (create only): 250ms
innerHTML (destroy & create): 14922ms
replaceHtml (destroy only): 31ms (474.3x faster)
replaceHtml (create only): 250ms (~ same speed)
replaceHtml (destroy & create): 297ms (50.2x faster)

I think the numbers speak for themselves. Comparable performance improvements can also be seen in Safari. In Opera, replaceHtml is still typically faster than innerHTML, but by a narrower margin. In IE, simple use of innerHTML is typically faster than mixing it with DOM methods, but not by nearly the same kinds of margins as you can see above. Nevertheless, IE's conditional compilation feature is used to avoid the relatively minor performance penalty, by just using innerHTML with that browser.

83 thoughts on “When innerHTML isn’t Fast Enough”

  1. Wow! I really didn’t expect it to be such a huge improvement in Firefox. Nice job!

    As to the performance in IE, perhaps you can use conditional compilation for that browser by adding the following lines just below the first statement:

    /*@cc_on
    oldEl.innerHTML = html;
    return oldEl;
    @*/

  2. Thanks, Dean Edwards and DrSlump! I’ve incorporated both of your comments into the code in this post and on the test page. I’m not sure why I didn’t think of creating a shallow copy with cloneNode()

  3. It’s slower in opera 9.23:

    15000 elements…
    innerHTML (destroy only): 31ms
    innerHTML (create only): 140ms
    innerHTML (destroy & create): 172ms
    replaceHtml (destroy only): 31ms (~ same speed)
    replaceHtml (create only): 156ms (1.1x slower)
    replaceHtml (destroy & create): 1328ms (7.7x slower)
    Done.

  4. Anyway, it’s better, because difference between Opera and FF is much less, than between FF and IE.

  5. @Luca, that depends on your system and background interference. On my system (Xeon 2.8GHz, 1GB RAM, WinXP), Opera 9.23 consistently runs the 15,000 element “destroy & create” test between 1.5 to 3.5 times faster with replaceHtml than with innerHTML. Note that if you run the test several times, generally only the fastest times are relevant (since averaging the cost of background interference is not very enlightening).

    The numbers you posted don’t add up. The “destroy & create” test should not be much different than the length of the “destroy only” and “create only” tests added together. That looks like a clear case of background interference.

  6. @Steve Brewer: As noted on Ajaxian, your currently posted test (which builds a single textNode consisting of the string “CONTENT” repeated 15,000 times) is not remotely comparable. Your version is certainly faster than plain innerHTML, at least in Firefox, but it is not faster than replaceHtml (it is easily demonstrable that the “destroy” step is slower).

  7. Hey Steve,
    Didn’t escape my HTML when I posted on blogger – I used 15,000 spans.

    You’re tests don’t show anything regarding innerHTML on a node that isn’t in the DOM. You’re method doesn’t have much if any performance gain over remove-update-replace; but it does have a big nasty side effect. For example, if you have event handlers bound to you node…

  8. “Didn’t escape my HTML when I posted on blogger” –Steve Brewer

    Gotcha, and I can see that in the updated post. Since on Ajaxian you asked me to demonstrate a difference, see here. At least on my system, withRemove() is consistently slower in each of the four big browsers. The difference isn’t as vast as with the comparison against simple use of innerHTML (although the “destroy only” test can be more than 10 times slower, and clearing elements is something I do regularly in RegexPal), but this was intended specifically to be as fast as possible when working with large numbers of elements/nodes.

    Your approach is probably preferable for JavaScript frameworks and such since it has less potential for unexpected side effects. However, if you don’t have event listeners attached to the specific element you’re updating or have numerous variables referencing it, replaceHtml() still seems the preferable approach.

    (BTW, since I’ve referenced the parallel discussion on Ajaxian several times, here’s a link for people who came here from elsewhere.)

  9. Just some tests in upcoming browsers. The script seems to perform really good in all of them. I’m pretty stunned how fast Opera is in comparison to the others. Firefox gets a real performance boost, the other browsers still get a remarkable speed boost.

    Safari 3:
    5000 elements…
    innerHTML (destroy only): 63ms
    innerHTML (create only): 390ms
    innerHTML (destroy & create): 484ms
    replaceHtml (destroy only): 47ms (1.3x faster)
    replaceHtml (create only): 15ms (26.0x faster)
    replaceHtml (destroy & create): 62ms (7.8x faster)
    Done.
    10000 elements…
    innerHTML (destroy only): 110ms
    innerHTML (create only): 3500ms
    innerHTML (destroy & create): 4735ms
    replaceHtml (destroy only): 110ms (~ same speed)
    replaceHtml (create only): 31ms (112.9x faster)
    replaceHtml (destroy & create): 141ms (33.6x faster)
    Done.

    Firefox 3:
    5000 elements…
    innerHTML (destroy only): 863ms
    innerHTML (create only): 522ms
    innerHTML (destroy & create): 1421ms
    replaceHtml (destroy only): 20ms (43.1x faster)
    replaceHtml (create only): 225ms (2.3x faster)
    replaceHtml (destroy & create): 239ms (5.9x faster)
    Done. 10000 elements…
    innerHTML (destroy only): 5521ms
    innerHTML (create only): 2626ms
    innerHTML (destroy & create): 8528ms
    replaceHtml (destroy only): 39ms (141.6x faster)
    replaceHtml (create only): 373ms (7.0x faster)
    replaceHtml (destroy & create): 422ms (20.2x faster)
    Done.

    Opera 9.5:
    5000 elements…
    innerHTML (destroy only): 16ms
    innerHTML (create only): 141ms
    innerHTML (destroy & create): 94ms
    replaceHtml (destroy only): 16ms (~ same speed)
    replaceHtml (create only): 78ms (1.8x faster)
    replaceHtml (destroy & create): 125ms (1.3x slower)
    Done.
    10000 elements…
    innerHTML (destroy only): 31ms
    innerHTML (create only): 156ms
    innerHTML (destroy & create): 312ms
    replaceHtml (destroy only): 31ms (~ same speed)
    replaceHtml (create only): 203ms (1.3x slower)
    replaceHtml (destroy & create): 157ms (2.0x faster)
    Done.

  10. My results with IE7

    1000 elements…
    innerHTML (destroy only): 0ms
    innerHTML (create only): 0ms
    innerHTML (destroy & create): 0ms
    replaceHtml (destroy only): 0ms (~ same speed)
    replaceHtml (create only): 0ms (~ same speed)
    replaceHtml (destroy & create): 0ms (~ same speed)
    Done.
    ——————————————————————————–
    15000 elements…
    innerHTML (destroy only): 31ms
    innerHTML (create only): 156ms
    innerHTML (destroy & create): 172ms
    replaceHtml (destroy only): 32ms (~ same speed)
    replaceHtml (create only): 157ms (~ same speed)
    replaceHtml (destroy & create): 188ms (1.1x slower)
    Done.

  11. @DelfinoM, no kidding. 🙂 If you look at the code, you will see that for IE it simply uses innerHTML with no tricks, since that’s already the fastest approach in that browser.

  12. I consider this a bug in Firefox. There’s no reason why .innerHTML = “” shouldn’t be the fastest method, or at least comparable in speed.

  13. Do you think this idea is worth submitting as a patch to Prototype? It seems like Element.Update could benefit from this technique.

  14. I found an interesting thing: If you put the elements in one single element, the destroying of the elements work with innerHTML nearly at same speed as replaceChild. It seems that Firefox with innerHTML destroys every direct child Element separate and not as a block.

  15. So, what do you do when ReplaceHtml() isn’t fast enough?

    Well, maybe that means that JavaScript isn’t the way to do it, but I have a generic query form/javascript, that I use for generating various reports, and I’d like to be able to refresh the reports, based on the user selections in the query form, without having to repopulate all the form selects (each one is at least a simple query and there may be many of them depending on which ones a report uses, and the report queries are quite complex, so any time savings anywhere is a benefit).

    Generally this works fine, but, depending on the user selections in the query form, the number of rows in the report can become quite large (and I’d like to avoid paging for a number of reasons), and, at some undetermined threshold, the performance replacing the innerHTML of the report table just completely tanks and Firefox, at least, will hang for 5-10 minutes on this line:

    oldEl.parentNode.replaceChild(newEl, oldEl);

    The innerHTML of oldEl is at this point quite small, just a handful of rows displaying progress information, so I suspect that the time is mostly spent on re-building, -rendering newEl (a table). (And, just setting the innerHTML of oldEl is just as slow as using ReplaceHtml() to do it.) Right now I’ve simply disabled the JavaScript refresh on the reports where this is a possible problem.

    Any ideas or suggestions on workarounds to get acceptable performance?

  16. You should be aware that the performance of DOM and innerHTML depends on many things – i.e. the amount of data appended to the elements. Unfortunately the site is in Danish, but I have tried to look a little deeper in the myth of innerHTML’s speed:
    http://www.dengodekode.dk/artikler/DOM/no_innerhtml.php#innerhtml_performance

    In the first of two tests I inserted 2000 element structures like this:
    <p><strong>5 characters</strong></p>
    <div>5 characters<span>5 characters</span>3 characters</div>
    <em>4 characters</em>

    I tested four different methods in IE and Firefox:
    *) innerHTML with string concatenation
    *) innerHTML with an array of elements, joined at the end of the script
    *) DOM – creating single elements in every iteration of the loop
    *) DOM – cloning a template

    In this test innerHTML was by far the fastest. In IE the string-concat method was quite slow – but still faster than DOM.

    The same tests were done with 500 structures like this:
    <p><strong>777 characters</strong></p>
    <div>1627 characters<span>1262 characters</span>98 characters</div>
    <em>339 characters</em>

    This time the picture was quite different:
    In Firefox DOM was 10-15 times faster than innerHTML!
    In IE DOM was 10 times faster than innerHTML with string-concat
    – but half as fast as innerHTML with a joined array

    Werner Heisenberg once said something like:
    “What we observe is not nature in itself but nature exposed to our method of questioning”

    – it seems to go quite well with webcode too ;o)

    /best regards

  17. FireFox & innerHTML

    How many chars were you able to write into an element with innerHTML? On other browsers it works quite well, but firefox seems to have trouble to write more then 4100 characters with innerHTML=.

  18. Hehe … Firefox is f****ed in many ways – although it’s highly politically incorrect to say so ;o)
    You can i.e. clone the whole documentElement and append it to a meta element in the head! Or how about cloning (or building it in DOM) a table – appending it to an option element – and get it rendered in the select element with images, form elements and all …?!??!!! Often the innerHTML will not tell you the trouth about what really has happended, but if you check the DOM, you will experience strange scenes before og your very eyes! =8-O

    Firefox divides the innerHTML string into chunks of 4096 – and puts these chunks into text nodes when inserting with innerHTML. Try this code:

    function foo() {
    var ss = “”, s = “Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec venenatis lectus quis lectus”;
    while (ss.length<90000) {
    ss += s;
    }
    gE(“fooBar”).innerHTML = ss;
    alert(“Number of text-nodes: ” + gE(“fooBar”).childNodes.length)
    alert(“Length of first text-node: ” + gE(“fooBar”).firstChild.nodeValue.length)
    alert(“Length of innerHTML: ” + gE(“fooBar”).innerHTML.length)
    }
    function bar() {
    var ss = “”, s = “Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec venenatis lectus quis lectus”;
    while (ss.length<90000) {
    ss += s;
    }
    gE(“fooBar”).appendChild( document.createTextNode(ss) );
    alert(“Number of text-nodes: ” + gE(“fooBar”).childNodes.length)
    alert(“Length of first text-node: ” + gE(“fooBar”).firstChild.nodeValue.length)
    alert(“Length of innerHTML: ” + gE(“fooBar”).innerHTML.length)
    }

    Test foo
    Test bar

    NB: Reload the page between testing ‘foo’ and ‘bar’

  19. Sorry! I have some JS-wrappers in my editor’s HTML-template – and I used one of them in the example above:

    var d=document;
    function gE(id){return d.getElementById(id)};

  20. – and forgot HTML-escaping :o|

    <button onclick=”foo()”>Test foo</button>
    <button onclick=”bar()”>Test bar</button>

    <div id=”fooBar”></div>

  21. I chanced upon the site and being an airhead, I have no ideas what you guys are talking about but it is quite interesting to see such passion you guys have for the topic on hand. Keep it up (so to keep like-minded friend of mine alive)!

  22. So, does replaceHtml leak memory if event handlers are attached to the node or its children? Just in IE? Or is the method merely slower if event handlers are present?

    (This question is in reference to Steve Brewer’s “but it does have a big nasty side effect. For example, if you have event handlers bound to you node…” comment. I’m not clear on what that means.)

    Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *