Efficient Javascript: D E V - O P E R A
Efficient Javascript: D E V - O P E R A
Efficient Javascript: D E V - O P E R A
V R E E D O
Lo g in We b Ad d - o ns Mo b ile TV Lab s
Efficient JavaScript
Traditionally, a Web page would not contain much scripting, or at least, not much that would affect the performance of that Web page. However, as Web pages become more like applications, the performance of scripts is having a bigger effect. With more and more applications being developed using Web technologies, improving the performance of scripts is becoming increasingly important. With a desktop application, a compiler is normally used to convert the source into the final binary. The compiler can take its time, and optimiz e as much as possible for good performance of the final application. Web applications do not have that luxury. Since they need to run on multiple browsers, platforms, and architectures, they cannot be completely precompiled. The browser has to do the interpretation and compilation each time it retrieves a script, and yet the final application has to run as smoothly as a desktop application, and load quickly as well. It is expected to run on a large variety of devices, from an ordinary desktop computer, to a mobile phone. Browsers are fairly good at achieving this, and Opera has one of the fastest scripting engines of any current browser. However, browsers do have their limits, and that is where the Web developer has to take over. Ensuring that a Web application runs as fast as possible can be a simple matter of trading one type of loop for another, making one combined style change instead of three, or adding only the script that will actually be used. This article will show several simple changes that can be made to improve the performance of your Web applications. Areas covered will be ECMAScript - the core language used by JavaScript, DOM, and document loading.
Quick Tips
ECMAScript
1. Avoid using eval or the Function constructor
PDFmyURL.com
1. Rewrite that eval 2. If you want a function, use a function 2. 3. 4. 5. 6. 7. 8. 9. 10. Avoid using with Don't use try-catch-finally inside performance- critical functions Isolate uses of eval and with Avoid using global variables Beware of implicit object conversion Avoid for-in in performance- critical functions Use strings accumulator- style Primitive operations can be faster than function calls Pass functions, not strings, to setTimeout() and setInterval()
DOM
1. Repaint and reflow 1. Keeping the number of reflows to a minimum 2. Minimal reflow 2. 3. 4. 5. 6. 7. 8. 9. 10. Document tree modification Modifying an invisible element Taking measurements Making several style changes at once Trading smoothness for speed Avoid inspecting large numbers of nodes Improve speed with XPath Avoid modifications while traversing the DOM Cache DOM values in script variables
Document loading
1. 2. 3. 4. 5. Avoid keeping alive references from one document to another Fast history navigation Use XMLHttpRequest Create SCRIPT elements dynamically location.replace() keeps the history under control
PDFmyURL.com
ECMAScript
Avoid using eval or t he Function const ruct or
Each time eval or the Function constructor is called on a string representing source code, the script engine must start the machinery that converts the source code to executable code. This is usually expensive for performance - easily a hundred times more expensive than a simple function call, for example. The eval function is especially bad, as the contents of the string passed to eval cannot be known in advance. Since the code is interpreted in the context of the call to eval this means that the compiler cannot optimise the surrounding context, and the browser is left to interpret much of the surrounding code at runtime. This adds an additional performance impact. The Function constructor is not quite as bad as eval, since using it does not affect the code surrounding the use, but it can still be quite slow.
This code performs exactly the same function, but avoids using eval:
The code that does not use eval performs around 95% faster than the original in Opera 9, Firefox, and Internet Explorer, and around 85% faster in Safari. (Note that this does not include the time needed to call the function itself.)
PDFmyURL.com
This code provides the same functionality, but avoids using the Function constructor. This is done by creating an anonymous function instead, which can be referenced just like any other object:
function addMethod(oObject,oProperty,oFunction) { oObject[oProperty] = oFunction; } addMethod(myObject,'rotateBy90',function () { this.angle=(this.angle+90)%360; }); addMethod(myObject,'rotateBy60',function () { this.angle=(this.angle+60)%360; });
var oProperties = ['first','second','third',...,'nth'], i; for( i = 0; i < oProperties.length; i++ ) { try { test[oProperties[i]].someproperty = somevalue; } catch(e) { ... } }
In many cases, the try-catch-finally construct could be moved so that it surrounds the loop. This does change the semantics a little, since if an exception is
PDFmyURL.com
thrown, the loop will be halted, although code following it will continue to run:
var oProperties = ['first','second','third',...,'nth'], i; try { for( i = 0; i < oProperties.length; i++ ) { test[oProperties[i]].someproperty = somevalue; } } catch(e) { ... }
In some cases, the try-catch-finally construct could be avoided completely by checking for properties, or using another appropriate test:
var oProperties = ['first','second','third',...,'nth'], i; for( i = 0; i < oProperties.length; i++ ) { if( test[oProperties[i]] ) { test[oProperties[i]].someproperty = somevalue; } }
It can be tempting to create variables in the global scope, simply because that is easy to do. However, there are several reasons why this can make scripts run more slowly. To begin with, if code inside a function or another scope references that variable, the script engine has to step up through each scope in turn until it reaches the global scope. A variable in the local scope will be found more quickly. Variables in the global scope also persist through the life of the script. In the local scope, they are destroyed when the local scope is lost. The memory they use can then be freed by the garbage collector. Lastly, the global scope is shared by the window object, meaning that it is in essence two scopes, not just one. In the global scope, variables are always located using their name, instead of using an optimiz ed predefined index, as they can be in local scopes. A global variable will take longer for the script engine to find, as a result. Functions are also usually created in the global scope. This means that functions that call other functions, that in turn call other functions, increase the number of times the script engine has to step back to the global scope to locate them. Take this simple example, where i and s are in the global scope, and the function uses those global variables:
This alternative version performs measurably faster. In most current browsers, including Opera 9, and the latest versions of Internet Explorer, Firefox, Konqueror and Safari, execution is about 30% faster than the original.
This equivalent example creates just a single object, and will perform better as a result:
If your code calls methods of literal values very often, you should consider converting them into objects instead, as in the previous example. Note that although most of the points in this article are relevant to all browsers, this particular optimiz ation is aimed mainly at Opera. It may also affect some other browsers, but can be a little slower in Internet Explorer and Firefox.
The for-in loop has its place, but is often misused, when a normal for loop would be more appropriate. The for-in loop requires the script engine to build a list of all the enumerable properties, and check for duplicates in that list, before it can start the enumeration. Very often, the script itself already knows what properties must be enumerated. In many cases, a simple for loop could be used to step through those properties, especially if they are named using sequential numbers, such as with an array, or an object that is given properties to make it appear to be an array (an example would be a NodeList object created by DOM). This is an example of incorrect use of a for-in loop:
var oSum = 0; var oLength = oArray.length; for( var i = 0; i < oLength; i++ ) { oSum += oArray[i]; }
a += 'x' + 'y';
That code would be evaluated by firstly creating a temporary string in memory, assigning the concatenated value of 'xy', then concatenating that with the current value
PDFmyURL.com
of a, and finally assigning the resulting value of that to a. The following code uses two separate commands, but because it assigns directly to a each time, the temporary string is not used. The resulting code is around 20% faster in many current browsers, and potentially requires less memory, as it does not need to temporarily store the concatenated string:
a += 'x'; a += 'y';
Primit ive operat ions can be f ast er t han f unct ion calls
Although not significant in normal code, there is a potential for improved speed in performance critical loops and functions, by replacing function calls with an equivalent primitive operation. An example would be the push method of an array, that is slower than simply adding an item to the index at end of the array. Another example would be methods of the Math object, where in most cases, simple mathematical operators would be more appropriate.
setInterval('updateResults()',1000);
PDFmyURL.com
setTimeout('x+=3;prepareResult();if(!hasCancelled){runmore();}',500);
In the first case, the function can simply be referenced directly. In the second case, an anonymous function can be wrapped around the code:
Note that in all cases, the timeout or interval delay may not be honoured exactly. In general, browsers will take a little longer than the requested delay. Some may compensate for that with intervals by firing the next one slightly early instead. Others will simply try to wait for the correct amount of time every time. Factors such as CPU speed, thread states, and JavaScript load will affect the accuracy of the delay. Most browsers will be unable to give a delay of 0 ms, and may impose a minimum delay, typically between 10 and 100 ms.
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.
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 siz e 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.
var docFragm = document.createDocumentFragment(); var elem, contents; for( var i = 0; i < textlist.length; i++ ) { elem = document.createElement('p');
PDFmyURL.com
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[i]); elem.appendChild(contents); cloned.appendChild(elem); } original.parentNode.replaceChild(cloned,original);
posElem.style.display = 'none'; posElem.appendChild(newNodes); posElem.style.width = '10em'; ... other changes ... posElem.style.display = 'block';
Taking measurement s
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 ...
var toChange = document.getElementById('mainelement'); toChange.style.background = '#333'; toChange.style.color = '#fff'; toChange.style.border = '1px solid #00f';
PDFmyURL.com
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 changes 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); }
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 necessary 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.
var allElements = document.getElementsByTagName('*'); for( var i = 0; i < allElements.length; i++ ) { if( allElements[i].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[i].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,
PDFmyURL.com
var allChildren = document.getElementById('inhere')h3 id=.childNodes; for( var i = 0; i < allChildren.length; i++ ) { if( allChildren[i].nodeType == 1 && allChildren[i].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.
var allElements = document.getElementsByTagName('*'); for( var i = 0; i < allElements.length; i++ ) { if( allElements[i].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() ) {
PDFmyURL.com
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[i].tagName.match(/^h[2-4]$/i) ) { ... } } }
allPara[i].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[i]; } for( i = 0; i < collectTemp.length; i++ ) { collectTemp[i].appendChild(document.createTextNode(i)); } collectTemp = null;
= = = =
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:
= = = =
Document loading
Avoid keeping alive ref erences f rom one document t o anot her
If one document has accessed nodes or other objects from another document, avoid retaining those references after the script has finished using them. If a reference was stored in a global variable or a property of any long- living object in the current document, clear it by setting it to null, or deleting it. The reason is that if the other document is destroyed, for instance if it was displayed in a popup window and that window is closed, any references to objects from that document will usually keep its entire DOM tree and scripting environment alive in RAM, even though the document itself is no longer loaded. The same will apply to pages within frames, inline frames, or OBJECT elements.
var remoteDoc = parent.frames['sideframe'].document; var remoteContainer = remoteDoc.getElementById('content'); var newPara = remoteDoc.createElement('p'); newPara.appendChild(remoteDoc.createTextNode('new content')); remoteContainer.appendChild(newPara); //remove references remoteDoc = null; remoteContainer = null; newPara = null;
submitted, a menu that stops working after an item has been clicked, or a page fadeout effect that leaves the page content obscured or invisible. A simple approach would be an onunload listener that resets the fading effect, or re- enables the form control. However, note that with some browsers, such as Firefox and Safari, adding a listener for the unload event will disable their fast history navigation. In addition, the act of disabling the submit button will be enough to disable fast history navigation in Opera.
document.getElementById('nextlink').onclick = function () { if( !window.XMLHttpRequest ) { return true; } var request = new XMLHttpRequest(); request.onreadystatechange = function () { if( request.readyState != 4 ) { return; } var useResponse = request.responseText.replace( /^[\w\W]*<div id="container">|<\/div>\s*<\/body>[\w\W]*$/g , '' ); document.getElementById('container').innerHTML = useResponse; request.onreadystatechange = null; request = null;
PDFmyURL.com
if( document.createElement && document.childNodes ) { document.write('<script type="text\/javascript" src="dom.js"><\/script>'); } if( window.XMLHttpRequest ) { document.write('<script type="text\/javascript" src="xhr.js"><\/script>'); }
location.replace('newpage.html');
PDFmyURL.com
Note that the page may still remain in cache, and may use memory there, but this will not be quite so much as if it were also kept in history.
This article is licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license.
Comments
The forum archive of this article is still available on My Opera.
anupshah9
Friday, December 2, 2011
Good article. Thanks for putting it together. One comment I have is about the XPath example - ironically for an efficiency article, the XPath is not very efficient. // in xpath hampers performance (a lot for large documents). Sometimes it cannot be avoided. But instead of //h2 you can use /html/body//h2. Alternatively you can set the context to be document.body instead of document and use .//h2 as the xpath (the dot is important!) (or .//h2 | .//h3 etc)
alvincrespo
Friday, December 2, 2011
Awesome article, lots of great tips here for improving the performance of any app. However I would like to see some more resources on XPath integration. Seems like something that we can all consider, but seeing some resources on how this works would be great.
wanhalo
Saturday, December 3, 2011
Paul Irish
Saturday, December 3, 2011
I think you overestimate the cost of reflows and repaints. But I think it's hard to put into context some of these recommendations. How much does 100 superfluous repaints cost compared to 10 try/catch's compared to 1 extra stylesheet reference in the ? Hard to say, but I wish we had better research and answers around comparing these recommendations to eachother. :)
Vojta Jina
Monday, December 5, 2011
I'm not sure with the primitive string vs string object comparison. Check out this test: http://jsperf.com/using- primitive- type- methods
@Vojta You have to test with more than just the modern browsers. I have run your tests in IE8 and not only are the the ops/sec much smaller but the String object test runs 2x more operations than any other tests.
Vojta Jina
Monday, December 5, 2011
@Todd Thanks for running it on IE8. Are you saying, you would use "new String()" to get slightly better perf on IE8 and loose on Chrome, Opera, Firefox, Safari (roughly 7x) ? Note, that using "charAt()" is faster with primitive even on IE8, only index access is faster. Sorry, I'm not buying this :- D Anyway, this is not big thing, as when it comes to perf, there are more important things, as this article correctly pointed out.
ariehg
Friday, December 9, 2011 PDFmyURL.com
About XPath - why not use querySelectorAll? I see only rare cases where XPath gives me more control than css selectors, while they are clearly a much more adopted language amongst client- side developers than XPath
mattyod
Friday, December 9, 2011
The item on implicit object conversion doesn't really seem to hold true anymore, even for IE8. http://jsperf.com/beware- of- implicit- object- conversion though this article does appear to be over 5 years old: http://web.archive.org/web/20061206200850/http://dev.opera.com/articles/view/efficient- javascript/ You must be logged in to write a comment. If you're not a registered member, please sign up.
Aut hor: Mark 'Tarquin' Wilton- Jones Dat e: Not yet published. Tags: efficient fast javascript optimal Languages: Japanese
Privacy
PDFmyURL.com