JavaScript best practices

In this section, we'll provide some tips and best practices for JavaScript.

Use literal notation for objects and arrays

You can choose to create objects and arrays in many ways, but the simplest and fastest way is to use literal notation. The traditional method uses the built-in constructors Object() and Array() as in the following example:

//create an object
var obj = new Object();
obj.debug = false;
obj.lang = "en";

//create an array
var arr = new Array("one", "two", "three"); 

Although the traditional method of creating objects and arrays is technically accurate, the literal notation evaluates more quickly and requires less code. Here's the previous code written using literal notations:

//create an object
var obj = {debug: false, lang: "en"};

//create an array
var arr = ["one", "two", "three"];

Avoid using global variables and functions

You can reduce the risk of name collisions and improve the performance of your application by binding properties and methods to a namespace object rather than creating global variables and functions.

Namespace collisions occur when two regions of code use the same global variable name for different purposes. In JavaScript, variables defined outside a function or object are globally available and are part of the global namespace. As your application grows and you add libraries to it, the risk of naming collisions increases.

Using global variables can also affect the performance of your script. If code inside a function or another scope references a particular global variable, the script engine must go through each scope until it reaches the global scope. A variable in the local scope is found more quickly. Variables in the global scope also persist through the life of the script, whereas local scope variables are destroyed when the local scope is lost. The memory that local variables use can then be freed by the garbage collector.

The following code snippet defines a function and variables that are global in scope. As a result, it is both inefficient—since it persists in memory much longer than necessary—and at risk for future namespace collisions.

//define global variables
var lang = "en";
var debug = true;

//define global function
function setLang (arg) {
    lang = arg;
}

In the following example, we've rewritten the previous snippet to define and use a namespace. The namespace object, myApp, is created and assigned values and functions to it. By using this technique, we limit the scope of the variables and functions.

var myApp = {
    lang: "en",
    debug: true,
};

myApp.setLang = function (arg) {
    this.lang = arg;
}

Structuring your code this way helps avoid name collisions and as your code base grows, you can neatly separate the objects into logical files.

Use try-catch blocks effectively

You can use try-catch statements to intercept thrown errors before they are handled by the browser. The try-catch statements are useful when you are trying to hide errors from the user, or when you want to produce custom errors for the benefit of your user.

When an error occurs in the try block, execution immediately stops and jumps to the catch block, where the error object is provided. When the catch block is executed, the error object is assigned to a new variable and that new variable lasts for the duration of the catch block; it's destroyed when the catch block ends. The creation and handling of this new runtime variable might affect the performance of your application. You should avoid using try-catch blocks within a critical function or loop.

The following example shows a loop than might throw several exceptions affecting the performance of the script:

var object = ['foo', 'bar'], i;
for (i = 0; i < object.length; i++) {
   try {
      // do something
   } catch (e) {
      // handle exception
   }
}

We've re-written the previous code to improve the effectiveness of the try-catch block. Instead of including the try-catch block within the loop, we restructure the code so that the try-catch block surrounds the loop.

var object = ['foo', 'bar'], i;
try {
    for (i = 0; i < object.length; i++) {
        // do something
    }
} catch (e) {
    // handle exception
}

Concatenate strings with the assignment operator

Concatenating, or joining strings, is commonly used in JavaScript, but the way you concatenate strings can affect the peformance of your application. You can concatenate strings in any of the following ways:

Method Example
Using the concatenation (+) operator str = "h" + "e";
Using the shorthand assigment (+=) operator str += "l";
Using string.concat() str = str.concat("l", "o");
Using array.join() str = ["h", "e", "l", "l", "o"].join("");

If you're concatenating just a few strings, then any of the above methods are acceptable and perform equally. However, if the length and number of strings increase then you should optimize how you concatenate strings.

image Slower: Concatenating strings with + operator

str += "x" + "y";

When you concatenate strings by using the concatenation (+) operator, the following occurs:

  1. A temporary string is created in memory,
  2. The concatenated value ("xy" in the example below) is assigned to the temporary string.
  3. The temporary string is concatenated with the variable's current value.
  4. The result is assigned to the variable.

image Faster: Concatenating strings with += operator

You can speed up concatenation and use less memory by avoiding the temporary string. In the following example, we create two distinct statements and directly assign the result with the assignment operator.

str += "x";
str += "y";

Optimize your loops

When you're using loops in JavaScript, you can optimize the overall performance of the loop by reducing the work that's done in each iteration. One way to reduce this work is to minimize the number of object lookups by caching the objects.

In the following example, we create a typical but inefficient loop. For each iteration of the loop, the arr.length object is re-calculated, causing an unncessary hit on performance.

for (var i = 0; i < arr.length; i++) {
    // length of arr is recalculated every time
}

You can improve the previous loop by caching the arr.length object so it's calculated only once. Now, the control condition uses only local variables, thereby improving the speed of the loop.

for (var i = 0, len = arr.length; i < len; i++) {
    // cache the length of the array
}

To further improve your loop, consider executing the loop in reverse. If you're not concerned about the order in which the array is processed, you can improve the loop execution by reversing the order with the decrement operator and by combining the control conditions. In the following example, the control condition is reduced from two comparisons (is i < len and is the condition true?) to one (is the condition true?):

for (var i = arr.length; i--;) {
    // in reverse
}

Avoid using the eval() method

To improve the performance of your script, you should avoid using the eval() method. The eval() method evaluates a string of JavaScript code and executes it. The code can contain an expression or JavaScript statements.

The use of eval() is discouraged for the following reasons:

  • Poor performance: The eval() method must invoke the compiler to pass the string and execute the results. In addition, the evaluated code is not cached.
  • Security issues: The eval() method can make your code vulnerable to various injection attacks as it executes any code given to it. You should never use eval() with strings of unknown or untrusted origins.
  • Impairs debugging: Code that includes eval() can be more difficult to debug because the code from eval() is dynamically generated. Also, the eval form is harder to read and can often be written in a cleaner and clear way without the use of eval().

image Incorrect usage: Using eval to set a value

eval("myValue = myObject." + myKey + ";");

image Correct usage: Using subscript notation to set a value

myValue = myObject[myKey];

The timeout methods setTimeout() and setInterval() can also take a string and execute code thereby acting like the eval() method. You should avoid passing a string to setInterval() and setTimeout() and instead pass a function.

image Incorrect usage: Passing a string to setInterval()

var oElement = null;
setInterval('oElement = document.getElementById("pepe");', 0);

image Correct usage: Passing a function to setInterval()

var oElement = null;
setInterval(function() {
    oElement = document.getElementById("pepe");
}, 0);

Use event delegation

When handling DOM events, you can improve the efficiency and performance of your script by attaching an event to a single parent element instead of attaching events to every element. This is called event delegation. Event delegation takes advantage of event bubbling to assign a single event handler to manage all the events of a particular type. For example, if there are 10 buttons inside a <div> element, you can attach one event listener to the <div> as opposed to attaching 10 listeners for the 10 buttons.

The HTML in the following example contains two buttons that should perform an action when clicked. Traditionally, we would simply attach an event handler to each item like this:

<a href="javascript:handleClick();">Click</a>
<button id="btn1" onclick="handleClick();">One</button>
<button id="btn2" onclick="handleClick();">Two</button>

To improve the performance of this code, we use event delegation by adding a single onclick event handler to the div that contains the buttons. Since all the buttons are children of the div, their events bubble up and are handled by the function. The event target is the button that was clicked, so we can check the id property to determine the appropriate action. With this approach, we retrieved one DOM element and attached a single event handler, thereby reducing the amount of memory used.

<div id="btngroup">
  <button id="btn1">One</button>
  <button id="btn2">Two</button>
</div>
document.getElementById("btngroup").addEventListener("click", function (event) {
  switch (event.srcElement.id) {
  case "btn1":
    handleClick();
    break;
  default:
    handleClick;
  }
}, false); // type, listener, useCapture (true=beginning, false=end)

Using event delegation also makes it easier to add and remove buttons in the future.

Minimize DOM access

Working with the DOM at any point can be a performance drainer. The DOM is a complex API that manages a lot of information, so even minor operations can take longer to execute because they often require re-rendering the page. To improve the performance of your application you should try to minimize your interactions with the DOM. Here are some tips for minimizing those interactions:

Reduce the number of DOM elements

The size of the DOM slows down all operations related to it, including manipulations, reflows, and traversing the DOM. One way to improve these operations is to make your DOM smaller.

  • Avoid extra tags and nested tables.
  • Try to aim for less than 500 elements in your DOM. You can track the size with document.getElementsByTagName('*').length.

Cache references to accessed elements

Whenever you are accessing DOM elements, you should be caching the objects that you access. More often than not, your script will repeatedly access a certain object, as in the following example:

Slow:

for (var i = 0; i < document.images.length; i++) {
    document.images[i].src = "blank.gif";
}

In the above example, the document.images object is accessed multiple times. This is inefficient because for every iteration of the loop, the browser must look up the document.images object twice: once to read the .length property and ascertain if i < document.images.length and once to read and change the value of the .src property.

A more efficient version uses a variable to store document.images, which can then be accessed by your script.

Better:

var imgs = document.images;
for (var i = 0; i < imgs.length; i++) {
    imgs[i].src = "blank.gif";
}

Minimize reflows and repaints

In addition to reducing the number of DOM interactions, you can improve the performance of your application by minimizing the number of DOM updates, such as changing elements, removing elements, or adding new ones. Updates to the DOM can cause the browser to repaint the screen as well as reflow, which can be expensive operations.

A repaint (often called a "redraw") occurs when you change an element that modifies its visibility but does not affect its layout (for example, changing the background color). The browser recognizes the visual change and repaints the document.

A reflow involves changes that affect the layout of a portion of the page (or the whole page) and causes the reflow of all child and ancestor elements (for example, a table). With a reflow, the browser looks to see how your changes affect the overall layout of the document. The browser recalculates the geometrics of the changed element as well as the geometrics and positions of other elements that could be affected by the change.

What causes a reflow?

A reflow is needed whenever you change the layout of the page. A reflow can occur if you:

  • Add or remove a DOM node
  • Change the position of an element
  • Change the size of an element (by changing the margin, padding, border thickness, font, width, height, etc.)
  • Resize the browser window
  • Add or remove a stylesheet
  • Change the content (for example, a user types text in an input box)
  • Activate CSS pseudo-selectors such as :hover
  • Manipulate the class attribute
  • Utilize a script to manipulate the DOM
  • Retrieve a measurement that must be calculated (for example, calculating offsetWidth and offsetHeight)
  • Set a property on a style attribute

How do you minimize repaints and reflows?

Here are some CSS and JavaScript tips for minimizing reflows and repaints:

CSS tips

  • Change classes on the element you wish to style as low in the DOM tree as possible to limit the scope of the reflow to as few nodes as possible.
  • Avoid setting multiple inline styles and try instead to combine the styles into a external class, which causes only one reflow when the attribute is changed.
  • Apply animations to elements whose position are fixed or absolute as they don't affect the layout of other elements, so only a repaint will occur rather than a reflow.
  • Avoid using tables for layout. If you need to use a table for data, use a fixed layout (table-layout:fixed).

JavaScript tips

  • Cache computed styles.
  • For static styles, change the class names not the styles. For dynamic styles, edit the cssText property.
    // bad - changing the stle - accessing DOM multiple times
    var myElement = document.getElementById('mydiv');
    myElement.style.borderLeft = '2px';
    myElement.style.borderRight = '3px';
    myElement.style.padding = '5px';
    
    // good - use cssText and modify DOM once
    var myElement = document.getElementById('mydiv');
    myElement.style.cssText = 'border-left: 2px; border-right: 3px; padding: 5px;';
  • When you have a number of changes to apply to a DOM element, batch the changes and perform them outside the "live" document tree before any reflow-triggering actions.
    • Create a copy of the node you want to update by using cloneNode(), work on that copy, and then replace the old node with your updated copy.
      // slower - multiple reflows
      var list = ['foo', 'bar', 'baz'], elem, contents;
      for (var i = 0; i < list.length; i++) {
          elem = document.createElement('div');
          content = document.createTextNode(list[i]);
          elem.appendChild(content);
          document.body.appendChild(elem); // multiple reflows
      }
                  
      // faster - create a copy
      var orig = document.getElementById('container'),
          clone = orig.cloneNode(true), // create a copy
          list = ['foo', 'bar', 'baz'], elem, contents;
      clone.setAttribute('width', '50%');
    • Modify an invisible element. If you need to modify an element, temporarily change its display property to none, modify the element, and then set the display property to block. This technique temporarily removes the element from the document flow, thus reducing the number of reflows.
      // slower
      var subElem = document.createElement('div'),
          elem = document.getElementById('animated');
      elem.appendChild(subElem);
      elem.style.width = '320px';
                  
      // faster
      var subElem = document.createElement('div'),
          elem = document.getElementById('animated');
      elem.style.display = 'none'; // will not be repainted
      elem.appendChild(subElem);
      elem.style.width = '320px';
      elem.style.display = 'block';
    • Create a document fragment by using DocumentFragment() to make multiple changes to the DOM in a single operation. After you've made your changes, append that fragment to the original document.
      // slower
      var list = ['foo', 'bar', 'baz'], elem, contents;
      for (var i = 0; i < list.length; i++) {
          elem = document.createElement('div');
          content = document.createTextNode(list[i]);
          elem.appendChild(content);
          document.body.appendChild(elem); // multiple reflows
      }
                  
      // faster
      var fragment = document.createDocumentFragment(),
          list = ['foo', 'bar', 'baz'], elem, contents;
      for (var i = 0; i < list.length; i++) {
          elem = document.createElement('div');
          content = document.createTextNode(list[i]);
          fragment.appendChild(content);
      }
      document.body.appendChild(fragment); // one reflow

Last modified: 2014-10-09



Got questions about leaving a comment? Get answers from our Disqus FAQ.

comments powered by Disqus