UserJS.org

Efficient JavaScript code

Written on 2005-06-15 19:52 by tarquin. Last modified 2005-09-19 09:19

Opera has one of the fastest and most efficient JavaScript engines of any browser, but when you have multiple User JavaScripts installed, it is important that they run efficiently to keep Opera's performance as high as possible. Well written code will help to minimise the performance impact of User JavaScripts.

Fast loops

Loops are a fundamental part of many scripts, and in general they cause no problems at all. However, it is possible to make them run faster, by using an optimised conditional. Take these equivalent examples:

for( var i = 0; i < document.getElementsByTagName('tr').length; i++ ) {
  document.getElementsByTagName('tr')[i].className = 'newclass';
  document.getElementsByTagName('tr')[i].style.color = 'red';
  ...
}
var rows = document.getElementsByTagName('tr');
for( var i = 0; i < rows.length; i++ ) {
  rows[i].className = 'newclass';
  rows[i].style.color = 'red';
  ...
}

Neither of these are efficient. getElementsByTagName returns a dynamic object, not a static array. Every time the loop condition is checked, Opera has to reassess the object, and work out how many elements it references, in order to work out the length property. This takes a little more time than checking against a static number. Similarly, referencing the element at the specified index requires Opera to reassess the object to work out what is at the specified index. This requires the object to be reassessed three times for every one time through the loop. Here are some much better ways of doing this loop (the first of these is often the best):

var rows = document.getElementsByTagName('tr');
for( var i = 0, row; row = rows[i]; i++ ) {
  row.className = 'newclass';
  row.style.color = 'red';
  ...
}
var rows = document.getElementsByTagName('tr');
for( var i = rows.length - 1; i > -1; i-- ) {
  var row = rows[i];
  row.className = 'newclass';
  row.style.color = 'red';
  ...
}

Referencing elements

The DOM provides many ways to reference elements, and it is easy to get carried away with elaborate use of childNodes, siblings, parentNodes and tagNames. However, using the DOM tree like this is both slow, and unreliable, especially if elements are added or removed from the document. Wherever possible, use getElementById to reference the element, or if needed, get as close to the element as possible, then walk the last part of the DOM tree to reach it (assuming the page's content is static enough for this to be reliable). Remember that if the page adds some whitespace between element nodes, this will become an additional childNode, and you may need to use getElementsByTagName to ensure you reference the correct element.

You may need to obtain a reference to multiple elements. Say for example that you want to reference all header elements on the page. You can use getElementsByTagName('*'), but this will reference far too many elements, and will make your loops much slower as they check to see if it is the correct element. This is a bad way to do it, and should only be used if you need to reference the headers in the correct order:

var headers = document.getElementsByTagName('*');
for( var i = 0, oElement; oElement = headers[i]; i++ ) {
  if( oElement.tagName.match(/^h[1-6]$/i) {
    ...
  }
}

This would be much better done using a nested for loop:

for( var i = 1; i < 7; i++ ) {
  var headers = document.getElementsByTagName('h'+i);
  for( var j = 0, oElement; oElement = headers[j]; j++ ) {
    ...
  }
}

String matching

If you need to search a within one string to see if it contains another string, there are two basic ways to do it. The first is to use 'indexOf' to find the position of the substring within the string. The second is to use 'match' or an equivalent method to find if a regular expression pattern can be found in the string.

All string matching techniques are very fast, but they should be used carefully to avoid making them wasteful. In general, with very simple string matches in Opera, stringObject.indexOf is faster than stringObject.match. If searching for simple string matches, indexOf should be used instead of regular expression matching wherever possible.

You should avoid matching against very long strings (10KB+) unless you absolutely have to. If you are certain that the match will only occur in a specific portion of the string, take a substring, and compare against that, instead of the entire string.

Using regular expressions with large numbers of wildcards will make string matching noticably slower. Repeatedly replacing substrings is also costly, and if possible try to reduce the number of replace commands you use, and try to optimise into fewer, more efficient replace commands. In many cases, string replacement performed against the page's own scripts would be better done by using opera.defineMagicFunction or defineMagicVariable to override a variable. If possible, use these instead. There is no single rule to this, and each script will need to be dealt with on an indiviual basis.

Create once, use repeatedly

If you use the same regular expression or string repeatedly, you should create it once and store it in a variable that you can use when matching. This allows the browser to be faster and more efficient with its use of expression optimisations. In the following example, the two regular expressions are treated separately, and will cause the browser to waste resources creating two separate expressions that contain the same value:

if( a == 1 && oNode.nodeValue.match(/^s*extra.*free/g) ) {
  <span class="js-comment">//this creates the first copy</span>
} else if( a == 2 && oNode.nextSibling.nodeValue.match(/^s*extra.*free/g) ) {
  <span class="js-comment">//this creates a second copy</span>
}

This equivalent code would work identically, but would be more efficient, as the browser only needs to create one copy of the expression:

var oExpr = /^s*extra.*free/g;
if( a == 1 && oNode.nodeValue.match(oExpr) ) {
  <span class="js-comment">//this uses the existing variable</span>
} else if( a == 2 && oNode.nextSibling.nodeValue.match(oExpr) ) {
  <span class="js-comment">//this also uses the existing variable</span>
}

In general, there is no need to delete the stored expression once you have finished using it, or set it to null. The script engine will manage garbage cleanup or expression caching as required. Trying to delete it would only end up wasting time to perform an unnecessary operation.

Note that if defined inline in a loop, each instance of the regular expression is created only once, and automatically cached for use the next time through the loop. However, if you create multiple instances of the same expression within the loop, each one will be created and cached separately, as shown above.

for( var i = 0, oNode; oNode = oElement.childNodes[i]; i++ ) {
  if( oNode.nodeValue.match(/^s*extra.*free/g) ) {
    <span class="js-comment">//this creates the expression
    //it will be cached and re-used next time through the loop</span>
  }
}

This does not apply to the new RegExp syntax, which will always create a new copy of the expression, and is generally much slower than creating static expressions. Avoid using this in loops if at all possible.

eval is evil

The 'eval' method, and related constructs such as 'new Function', are extremely wasteful. They effectively require the browser to create an entirely new scripting environment (just like creating a new web page), import all variables from the current scope, execute the script, collect the garbage, and export the variables back into the original environment. Additionally, the code cannot be cached for optimisation purposes. eval and its relatives should be avoided if at all possible.

Only listen to what you need

Every time you add an event handler, the scripting engine needs to start listening and firing for that event. Additional listeners for the same event will add increased load on the engine. Adding unnecessary handlers will have some impact on performance, so you should register only what you need. Remember that many other User JavaScripts will also register handlers.

If you want to make modifications to several scripts on a page, you could add several 'BeforeScript' event listeners, one for each modification. However, especially since 'BeforeScript' is used by a substantial number of other User JavaScripts, it is much more efficient to use a single 'BeforeScript' event listener, and use it to make the required modifications to all the required scripts.

Adding an event listener for the 'BeforeEvent' event is the most wasteful of all, since it causes all possible events to fire, even if they are not needed. In general, this can be several thousand events per second. 'BeforeEvent' should be avoided at all costs, and replaced with the appropriate 'BeforeEvent.eventtype'. Duplicate listeners can usually be replaced with a single listener that provides the functionality of several listener functions.

Avoid running unnecessary code

If your script is only supposed to run if certain conditions are satisfied, check for those conditions as early as possible, and stop the script immediately if the conditions are not satisfied. If your script is inside a function (which most User JavaScripts should be), you can use return to stop the function.

Say for example that your script is designed to modify any of three possible scripts, but only if those scripts are contained in external files, you can check for the existence of the element's 'src' attribute and stop immediately if it is not present. Additionally, if you find that you have detected one of the scripts, you can make your modifications, and use 'return' when you are done. Now, to complicate matters, assume that you need to make the same modifications to two of the scripts, as well as making other modifications to each of them. You can make each modification (using 'else if' to make sure that the second if statement does not even need to be evaluated if the first was satisfied), then combine the final modification so that they both run it, without the need for any inefficiency:

window.opera.addEventListener('BeforeScript',function (e) {
  var oSrc = e.element.getAttribute('src');
  if( !oSrc ) { return; } <span class="js-comment">//no need to run any more</span>
  var oJs = e.element.text;

  if( oSrc.match(//firstScript.js$/) ) {
    e.element.text = oJs.replace(/Opera 7//g,'Opera 7/');
    return;
  }

  <span class="js-comment">//no need to use 'else' - that would be an extra command
  //and the earlier 'return' removes the need for 'else' here</span>
  if( oSrc.match(//secondScript.js$/) ) {
    oJs = oJs.replace(/ua.isIE=document.all/,'ua.isIE=false');

  } else if( oSrc.match(//thirdScript.js$/) ) {
    <span class="js-comment">/* the 'else if' ensures that if the first condition
    was satisfied, the browser does not even need to attempt
    to evaluate the second 'if' statement - saving time */</span>
    oJs = oJs.replace(/ua.isMoz=!document.all/,'ua.isMoz=true');
  }

  e.element.text = oJs.replace(/window.opera/g,'window.oopera');
},false);

There is also a similar way to break out of 'for' or 'while' loops, and conveniently, it is called 'break'. Say, for example, that you need to scan through all <link> tags on the page, and if their href attribute references a particular css file, replace it with another file. Once you have done that, there is no need to continue checking the other <link> tags. You could use 'return' to stop processing the function, but you might want to continue the rest of the function after the scanning loop. You could waste a little bit of memory by storing an extra variable that says if you have found the correct tag, and add that into your loop conditional. Depending on the circumstances, that may not be so simple, and it is more easy to just break out of the loop:

document.addEventListener('load',function () {
  var oLinks = document.getElementsByTagName('link');
  for( var x = 0, oLnk; oLnk = oLinks[x]; x++ ) {
    if( oLnk.getAttribute('href').match(//old.css$/) ) {
      oLnk.setAttribute('href','styles/new.css');
      break;
    }
  }
  <span class="js-comment">//now do something else useful here</span>
},false);

Timers take too much time

setInterval and setTimeout are commonly used to provide animation effects. Each one requires an additional thread to be created for each timeout, and when browsers try to run several timeout threads simultaneously, they slow down. With one or two timers, there is no problem, but with five or ten, the browser will start to feel very slow. Additionally, timers are in the eval family of methods, making them doubly inefficient.

If you can avoid using timers in User JavaScript, you should do so. If you need to use several timers that run simultaneously, try using a single timer that performs the functionality of all of the other timers.

Because a timer normally has to evaluate the given code in the same way as eval, it is best to have as little code as possible inside the evaluated statement. Instead of writing all of the code inside the timeout statement, put it in a separate function, and call the function from the timeout statement. This allows you to use the direct function reference instead of an evaluated string. As well as removing the inefficiency of eval, this will also help to prevent creating global variables within the evaluated code.

function doTimeoutStuff() {
  for( var i = 0; i < 5; i++ ) {
    document.getElementBy('collapse'+i).style.display = 'none';
  }
}
setTimeout(doTimeoutStuff,2000);

If using a function reference as the first parameter in the setTimeout or setInterval call, you can pass parameters to the function by putting them as the third, fourth, etc. parameters in the call to create the timeout.

function doTimeoutStuff(oFrom,oTo) {
  for( var i = oFrom; i <= oTo; i++ ) {
    document.getElementBy('collapse'+i).style.display = 'none';
  }
}
setTimeout(doTimeoutStuff,2000,0,4);

Take a short circuit

The short circuit operator (&&) helps you to optimise your conditional statements, allowing an expensive operation to be performed only if a less expensive condition check is successful. It ensures that the second condition is only evaluated if the first one is satisfied.

This is most often used to avoid errors by checking for properties before trying to check for their methods:

if( oElement.firstChild && ( oElement.firstChild.nodeValue == 'test' ) )

The short circuit operator also makes it possible to increase the efficiency of your scripts. Say for example, that you need to modify a script, but only if it contains a certain text pattern /bua.can(Not)?Do(/, and if the script file name or path contains the substring 'uacheck'. The first of these is a very expensive operation in terms of performance, as it requires the entire length of every external script file that is loaded to be matched against a regular expression. The second test is much less expensive, as not only is it a simple string match, it also needs to check only the file name, which will almost always be far smaller than the script file it references.

To maximise efficiency, you can check the filename first, and only go on to perform the more expensive file content check if the filename check is satisfied. This could be done in separate nested if statements, but it is easier and more efficient to do using a single if statement, and the short circuit operator:

window.opera.addEventListener('BeforeScript',function (e) {
  var eName = e.element.getattribute('src');
  var eSource = e.element.text;
  if( eName.indexOf('uacheck') && eSource.match(/bua.can(Not)?Do(/) ) {
    ...perform any required changes here...
  }
},false);

The || operator performs in a similar way, only evaluating the second condition if the first one is not satisfied. If you have two conditions, where only one of them needs to be satisfied for you to continue, you can put the least expensive one first, so that the second one will only be avaluated if the first one fails.

if( eName.indexOf('unsupported') || eSource.match(/bnav.excluded?s*==/) )

Reduce reflow

Every time you add an element to the document, the browser has to reflow the page to work out how everything has to be positioned and rendered. The more things you add, the more it has to reflow. If you can reduce the number of separate elements you add, the less the browser will have to reflow, and the faster it will perform.

If you are adding a new element that will have several children, instead of adding it to the document and then attaching its children to it, you should attach its children to it first, and then add it to the document. That way the browser only has to do a single reflow. If you need to add several sibling elements, not as children of another new element, you can use a document fragment, attach the elements to it, and then add the document fragment to the document. The elements will then be added as siblings, with a single reflow.

var foo = document.createDocumentFragment();
foo.appendChild(document.createElement('p'););
foo.appendChild(document.createElement('p'));
foo.firstChild.appendChild(document.createTextNode('Test'));
foo.lastChild.appendChild(document.createTextNode('Me'));
foo.firstChild.style.color = 'green';
document.body.appendChild(foo);

The same thing applies to text content for the elements, and any styles that will be applied to them. Wherever possible, add the content and styles first, and then add the element to the document.

Assign multiple styles

It is common to want to assign several styles to an element. Normally this is done with this wasteful syntax:

oElement.style.position = 'absolute';
oElement.style.top = '0px';
oElement.style.left = '0px';
... etc ...

This can often be reduced to a single assignment, saving time, reducing the number of assignments the browser needs to perform, and reducing it to a single reflow:

oElement.setAttribute('style','position:absolute;top:0px;left:0px;... etc ...');