Speed up your JavaScript, Part 4
Over the past few weeks, I’ve been exploring the various techniques for speeding up your JavaScript. Part 1 covered how to deal with loops that are doing too much. Part 2 focused on functions that do too much and taught techniques such as queuing and memoization to lighten the workload. Part 3 expanded the conversation to handling recursion both with memoization and switching to an iterative algorithm. Part 4, the last part in this series, focuses on too much DOM interaction.
We all know that the DOM is slow…really slow…and that it’s one of the most common sources of performance issues. What makes it slow is that DOM changes can change the user interface of a page, and redrawing the page is an expensive operation. Too many DOM changes mean a lot of redrawing since each change must be applied sequentially and synchronously to ensure the correct end result. This process is called reflow, and is one of the most expensive functions of a browser. Reflow happens at various points in time:
- When you add or remove a DOM node.
- When you apply a style dynamically (such as
element.style.width="10px"). - When you retrieve a measurement that must be calculated, such as accessing
offsetWidth,clientHeight, or any computed CSS value (viagetComputedStyle()in DOM-compliant browsers orcurrentStylein IE), while DOM changes are queued up to be made.
They key, then, is to limit the number of reflows that occur on a page via DOM interactions. Most browsers will not update the DOM while JavaScript is executing. Instead, they queue up the DOM interactions and apply them sequentially once the script has finished executing. As with JavaScript execution, the user cannot interact with the browser while a reflow is occurring. (Reflows will happen when the long-running script dialog is displayed because it represents a break in JavaScript execution, allowing the UI to update.)
There are two basic ways to mitigate reflow based on DOM changes. The first is to perform as many changes as possible outside of the live DOM structure (the part representing visible elements). The classic example is adding a number of DOM nodes into a document:
for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
list.appendChild(item);
}
This code is inefficient because it touches the live DOM each time through the loop. To increase performance, you should minimize this number. The best option, in this case, is to create a document fragment as an intermediate placeholder for the created li elements and then use that to add all of the elements to their parent:
var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
fragment.appendChild(item);
}
list.appendChild(fragment);
This version of the code touches the live DOM only once, on the last line. Prior to that, the document fragment is used to hold the intermediate results. Since a document fragment has no visual representation, it doesn’t cause reflow when modified. Document fragments also can’t be added into the live DOM, so passing it into appendChild() actually adds all of the fragment’s children to list rather than the fragment itself.
The second way to avoid unnecessary reflow is to remove a node from the live DOM before operating on it. You can remove a node from the live DOM in two ways: 1) literally remove the node from the DOM via removeChild() or replaceChild(), or 2) setting the display style to "none". Once the DOM modifications have been complete then the process must be reversed and the node must be added back into the live DOM. Another approach to the previous example could be:
list.style.display = "none";
for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
list.appendChild(item);
}
list.style.display = "";
Setting the list’s display to “none” removes it from the live DOM since it no longer has a visual representation. All of the items can safely be added before setting the display back to its default value.
Another common source of multiple reflows is making changes to an element’s appearance via the style property. For example:
element.style.backgroundColor = "blue";
element.style.color = "red";
element.style.fontSize = "12em";
This code has three style changes…and also three reflows. A reflow happens with every change in style to this element. If you’re going to be making a number of changes to an element’s style, it’s best to group those in a CSS class and then change the class using JavaScript rather than applying individual style changes manually. For example:
.newStyle {
background-color: blue;
color: red;
font-size: 12em;
}
Then the JavaScript becomes a single line:
element.className = "newStyle";
Changing the class of an element counts allows all of the styles to be applied at once, within a single reflow. This is much more efficient and also more maintainable in the long run.
Since the DOM is so slow at pretty much everything, it’s very important to cache results that you retrieve from the DOM. This is important for property access that causes reflow, such as offsetWidth, but also important in general. The following, for example, is incredibly inefficient:
document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft +
document.getElementById("myDiv").offsetWidth + "px";
The three calls to getElementById() here are the problem. Accessing the DOM is expensive, and this is three DOM calls to access the exact same element. The code would better be written as such:
var myDiv = document.getElementById("myDiv");
myDiv.style.left = myDiv.offsetLeft + myDiv.offsetWidth + "px";
Now the number of total DOM operations has been minimized by removing the redundant calls. Always cache DOM values that are used more than once to avoid a performance penalty.
Perhaps the most egregious offender of slow property access is the HTMLCollection type. This is the type of object that is returned from the DOM anytime a collection of nodes must be represented, and so is the type of the childNodes property and is the type returned from getElementsByTagName(). An HTMLCollection may act like an array in many ways, but it actually is a living, breathing entity that changes as the DOM structure changes. Every time you access a property on an HTMLCollection object, it actually queries the DOM for all nodes matching the original criteria once again. That means the following is an infinite loop:
var divs = document.getElementsByTagName("div");
for (var i=0; i < divs.length; i++){ //infinite loop
document.body.appendChild(document.createElement("div"));
}
This code is an infinite loop because every time a new div element is added to the document, the divs collection is updated with that new information. That means that i will never reach divs.length because divs.length increases by one every time through the loop. Every time divs.length is accessed, it collection is updated, making it far more expensive than accessing a regular array’s length property. When dealing with HTMLCollection objects, it’s best to minimize the number of times you access their properties. You can speed up a loop tremendously by simply caching the length in a local variable:
var divs = document.getElementsByTagName("div");
for (var i=0, len=divs.length; i < len; i++){ //not an infinite loop
document.body.appendChild(document.createElement("div"));
}
This code no longer represents an infinite loop because the value of len remains the same through each iteration. Caching the value is also more efficient so that the document isn’t queried more than once.
This wraps up the “Speed up your JavaScript” series. I hope you’ve learned enough to avoid the long-running script dialog and make your code much faster. A lot of the topics I’ve covered aren’t new; I’m just presenting them all in one place so that others can find this information easily. If you have other topics you’d like to see me cover, feel free to leave a note in the comments or contact me directly.
Translations
Disclaimer: Any viewpoints and opinions expressed in this article are those of Nicholas C. Zakas and do not, in any way, reflect those of my employer, my colleagues, Wrox Publishing, O'Reilly Publishing, or anyone else. I speak only for myself, not for them.
Both comments and pings are currently closed.




19 Comments
Excellent write up again, Nicholas. All four parts are valuable pieces to JavaScript optimization.
Did you mean in your examples to have the following:
item.appendChild(document.createTextNode(“Option ” + 0);
or did you mean “+ i” instead of “+ 0″?
I think you meant for the following:
item.appendChild(document.createTextNode(“Option ” + i);
Joseph McCann on February 3rd, 2009 at 10:54 am
> When you apply a style dynamically (such as element.style.color=”red”)
Actually, this won’t cause a reflow. Changing the metrics (e.g. width or height) of an element will cause a reflow. Changing text colour will not.
> When you retrieve a measurement that must be calculated
I’m pretty certain that this does not cause a reflow either.
> This code is inefficient because it touches the live DOM each time through the loop.
That’s not true either. So long as the loop does not yield processing, reflow does not occur until after the current execution context is complete.
It may be that the some of the points I mention are dependent on each other. e.g. If you add an element and then try to compute its style properties then you will get a reflow. But it is not quite so simple as you have laid out here.
Dean Edwards on February 3rd, 2009 at 1:15 pm
Thanks Joseph, I’ve fixed the code.
Nicholas C. Zakas on February 3rd, 2009 at 1:21 pm
Thanks for the first issue, Dean, that was just a poorly-selected example.
As for your other comments, I’ve found evidence that these are, in fact, true. Especially in the case of loops that touch the DOM. You’re right that not all browsers will reflow immediately (Opera will, though), however, simply interrogating the DOM for information is enough to cause a performance hit. Further, you’re building up a queue of reflows to happen once the process has gone idle, which ultimately slows down the entire user experience.
I will own up to over-simplifying this problem, but that is precisely the goal: to explain as succinctly as possible the results of my benchmarking.
Nicholas C. Zakas on February 3rd, 2009 at 1:37 pm
Nicholas, I’m pretty sure that reflows are not queued. If you perform lots of DOM manipulations within one execution context then you will only get one reflow once you exit the context. Perhaps you are getting different results because of your recent experiments with timers?
Dean Edwards on February 3rd, 2009 at 2:18 pm
Yes, that’s entirely possible. Perhaps it’s time to look at some source code rather than keep experimenting.
Nicholas C. Zakas on February 3rd, 2009 at 2:20 pm
Hi Nicholas,
I have a concern about “when reflow happens”. I thought that “taking measurements” will force a reflow immediately only when there is already such actions queued.
According to http://dev.opera.com/articles/view/efficient-javascript/?page=3, it should be in this way.
Morgan Cheng on February 4th, 2009 at 1:46 am
You are correct, Morgan. I thought I had put that in but apparently not. I’ve made the correction, thanks for pointing it out.
Nicholas C. Zakas on February 4th, 2009 at 10:34 pm
Hi Nicholas
Don’t know if you know this already but your argument about HTMLCollections also applies to normal arrays.
Further iterations are marginally faster when using — (decrementor) instead of ++ (incrementor).
All recursive functions can be unrolled to normal loops – recursion is beautiful but inheretly slow. If speed is an issue one should avoid it all together – not just minimize the usage of it.
I think you should mention that memoization is not nessesarily a good idea – there is a huge increase in memory usage which again makes it slow as the browser has to allocate/deallocate memory.
The specific case of computing fibonacci numbers can be done without recursion or loops simply be using its relation to the golden number. See http://www.jslab.dk/library/Math.fibonacci for an example.
About reflow: What if an element is positioned absolute? As I understand it is ‘taken out of the flow’. It doesn’t affect any other elements nor is it itself affected. Makes very little sense to reflow the page when changes are to an element with position abolute. Which is nice because everything which is moving around are usually positioned absolute. Can you confirm this?
Dok on February 5th, 2009 at 10:59 am
Thank you so much for the tips, I have incorporate some of it to my mini dom library.
http://github.com/kltan/yshort/tree/master
Also I was wondering if the createDocumentFragment() method can be used in the sample code below http://blog.stevenlevithan.com/archives/faster-than-innerhtml
Kean Tan on February 5th, 2009 at 9:09 pm
@Dok – thanks for the added insights, very useful. I’m not sure about the absolutely positioned element. The element itself is out of flow but it may have elements flowed within it. Also, absolutely positioned elements behave slightly different inside of relatively positioned elements, so that may have an effect. Good question though!
@Kean – document fragments wouldn’t really improve the approach mentioned there since it’s just replacing a single node with another.
Nicholas C. Zakas on February 5th, 2009 at 10:30 pm
[...] final part of the series deals with the DOM, and looks at practices such as using DOMFragments instead of touching the live [...]
Speeding up your JavaScript: Part 3 and 4 | How2Pc on February 7th, 2009 at 7:46 am
I’ve noticed that when I render huge tables in javascript the fastest way to do this is by assigning a value to innerHTML of the table’s container element. Why is this?
Artem Nezvigin on February 10th, 2009 at 11:47 am
@Artem – That’s because you’re instantiating the HTML parser just once, so most of the processing happens outside of JavaScript (and thus is much faster).
Nicholas C. Zakas on February 10th, 2009 at 2:47 pm
[...] ã€åŽŸæ–‡ã€‘Nicholas C. Zakas – Speed up your JavaScript, Part 4ã€è¯‘文】明达 – [...]
如何æå‡JavaScriptçš„è¿è¡Œé€Ÿåº¦ï¼ˆDOM篇) « 七月佑安 on March 15th, 2009 at 7:01 am
[...] ã€åŽŸæ–‡ã€‘Nicholas C. Zakas – Speed up your JavaScript, Part 4 ã€è¯‘文】明达 – [...]
speed-up-your-javascript-part-4 « Oragg.com on June 14th, 2009 at 10:45 am
[...] Mozilla Docs on Reflow Nicholas C. Zakas on Speeding Up your Javascript :css, DOM, JavaScript, [...]
Avoiding Reflow - XCacheGrind on March 31st, 2010 at 3:24 am
[...] up your JavaScript – Part 1Speed up your JavaScript – Part 2Speed up your JavaScript – Part 3Speed up your JavaScript – Part 4Further Info:Google Tech Talks YouTube ChannelSpeed up your JavaScript (article by the speaker [...]
Seven Must-See Videos and Presentations for Web App Developers - Smashing Magazine on July 18th, 2010 at 9:17 am
Between the article and the comments, I’ve picked up quite a few little tips here. Thanks!
Kyle A. Matheny on July 21st, 2010 at 11:40 am
Comments are automatically closed after 14 days.