<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=266211600481844&amp;ev=PageView&amp;noscript=1">

JavaScript Performance Coding Driven by Engine Optimizations

Oct 27, 2014 JavaScript David Aschliman

JS_blog_imageIn the world of dynamic client-side web development, there is hardly a more mainstream programming language than JavaScript. Fairly recent strides in browser rendering and code optimizations have made JavaScript a significant workhorse of the modern web. Because of the continued rise of this language and increased expectations of Ajax-driven applications, a web programmer must make a conscious effort to produce viable and efficient JavaScript code.

As with any development effort, we must keep in mind the target audience. Aside from other programmers, the target audience of JavaScript code in many contexts is the browser—or more specifically, the JavaScript engine. Being familiar with the expectations and habits of JavaScript engines enables us to tailor our code in a way that allows the engine to produce a quick, optimized result. There is a wide variety of JavaScript engines available, but the modern browsers that tend to get the most traction use only a handful. Let’s take a closer look at these engines, and see if a better understanding can help us refine our coding practices.

Understanding Engines

Researching specifics about the modern JavaScript engines V8, SpiderMonkey, Chakra, and Nitro reveal a number of commonalities that will direct our coding techniques. While all four of these engines take slightly different means of getting there, they all parse the JavaScript eventually into machine code for optimal execution. In general, many of them take an approach of quickly generating less efficient machine code and then later optimizing areas of the code that are used often.

The very dynamic nature of JavaScript, however, is what makes the engine’s job of optimization quite a chore. Since there is no guarantee of data type or structure at compile time (like in static languages), the engine must make assumptions about our code that could help it simplify execution. These assumptions are made and revised as our JavaScript code continues to run, and thus the machine code driving the script evolves as well. How we code strongly influences how easily the engine drives that evolution!

Performance Coding

It’s wonderful that we can program in JavaScript and not worry about how it works. But when it comes to achieving the best performance, knowing what the compiler is looking for makes all the difference. Below is a list of compiler-related suggestions that will help our code push the limits:

  1. Same Shape, Every Time

    One characteristic of an object that helps it stand apart from other is its shape. In JavaScript, an object’s shape is merely the number, names, and data types of its properties. Because our JavaScript object can be optimized using static-programming techniques (like using background classes), keeping the object shape the same is essential. Building your objects using constructors, and then not adding or removing properties helps to ensure that the proper optimizations can take place. Here are some examples:

    //Good: this ensures that all the properties of the object are the same from the start.
    //Just don't add/delete properties later.
    var Car = function () {
        this.doors = 2;
        this.color = 'red';
        this.transmission = 'auto';
    };
    var myNewCar = new Car();
    myNewCar.doors = 4;

    //Also Good: This works too if the object does not need to be reused
    var myNewCar = { doors: 2, color: 'red', transmission: 'auto' };
    myNewCar.color = 'black';

    //A bit sketchy, but technically this is fine as long as all the properties are always added in the same order
    var myNewCar = {};
    myNewCar.doors = 4;
    myNewCar.color = 'green';
    myNewCar.transmission = 'manual';

    //Bad...we are changing the shape!!!
    var myNewCar = new Car();
    myNewCar.maxSpeed = 210;
  2. Same Type, Every Time

    JavaScript compilers tend to optimize on a function level. Because static types are much more efficient to handle, the compiler attempts to make type-specific assumptions about arguments to an optimized function. By keeping the data type of the arguments the same every time a call is made, the machine code can be much more efficient and avoid costly type checking and casting. The same rule applies to any variable. Some examples follow:

    //Good: always calling with integers
    function combine(arg1, arg2, arg3) {
        return arg1 + arg2 + arg3;
    }
    combine(1, 2, 3);
    combine(2, 3, 5);

    //Not so Good: calling now with other types
    combine('not', 'so', 'good');
    combine(1.44, 8, 'yes');

    //Not so Good: variable holds values of multiple types throughout execution
    var foo = 'bar';
    foo = 3.14159;
  3. Divide and Conquer

    Because compilers optimize based on how often a function is used, be sure to divide your code to use common utility functions. If the compiler then considers the function to be heavily used, it can optimize it’s machine code, or choose to inline the function call (injects short functions directly where it was originally called). See the code examples below:

    //Good: the utility function is separated so it can be optimized
    function cleanUserNumberInput(val) {
        if (val && val.trim) {
            val = val.trim();
            //check if valid float
            if (/^(?:\d*\.\d+|\d+)$/.test(val)) {
                return parseFloat(val);
            }
        }
        return NaN;
    }
    var input1 = '154.33',
        input2 = '133',
        input3 = '9.9',
        input4 = '3';
        result = (cleanUserNumberInput(input1) * cleanUserNumberInput(input2)) + cleanUserNumberInput(input3);
    if (!isNaN(result)) {
        result = Math.pow(result, cleanUserNumberInput(input4));
    }
    alert('The answer is: ' + result);

    //Bad: the function is not extracted...not going to be optimized
    var input1 = '154.33',
        input2 = '133',
        input3 = '9.9',
        input4 = '3',
        isDecimalRegex = /^(?:\d*\.\d+|\d+)$/,
        clean1 = (isDecimalRegex.test(input1.trim()) ? parseFloat(input1.trim()) : NaN),
        clean2 = (isDecimalRegex.test(input2.trim()) ? parseFloat(input2.trim()) : NaN),
        clean3 = (isDecimalRegex.test(input3.trim()) ? parseFloat(input3.trim()) : NaN),
        clean4 = (isDecimalRegex.test(input4.trim()) ? parseFloat(input4.trim()) : NaN),
        result = (clean1 * clean2) + clean3;
    if (!isNaN(result)) {
        result = Math.pow(result, clean4);
    }
    alert('The answer is: ' + result);
  4. Avoid Sparse Arrays

    Whenever creating and using arrays, it is important to ensure they are populated sequentially. If not, the compiler may use a hash table behind the scenes, which could dramatically slow down performance. Below are a few examples:

    //Good: the array is initialized first and then added to sequentially
    var arr = ['the', 'quick', 'brown', 'fox', 'jumped'];
    arr[5] = 'over';
    arr[6] = 'the';
    arr[7] = 'lazy';
    arr[8] = 'dog';

    //Good: the array is initialized with a predefined length, then entries are added within that bounds
    var arr = new Array(100);
    arr[25] = 'index 25';
    arr[75] = 'index 75';

    //Bad: the array is initialized nonsequentially, and may become a sparse array
    var arr = [4, 5, 6];
    arr[150] = 12;
    arr[19] = 99;
  5. Avoid Unnecessary Try Statements

    Be aware that compilers often do not optimize any functions that contain a try statement. Obviously sometimes these are necessary, but adding them in without need shackles the optimizations the compiler can perform. See the below example:

    //these functions are intending the same thing, but one uses a try statement and cannot be optimized
    function goodFunc(a, b) {
        var result = a / b;
        return result;
    }

    //try catch is unnecessary since invalid parameters already either yeild Infinity or NaN
    function notGoodFunc(a, b) {
        try {
            return a / b;
        }
        catch (ex) {
            return NaN;
        }
    }
  6. Minimize DOM Interaction

    Realizing that the DOM is handled by the browser itself, and the JavaScript is handled by the engine, we need to minimize the amount that we interact with DOM elements in order to reduce the overhead of communication between these two components. This is especially true when using DOM-reliant plugins like jQuery. Consider doing manipulations in batch rather than individually. See a few examples below:

    //Making DOM Changes in Batch (see fiddle)
    //Good: create a new element, make changes to it, then add it to the DOM at the end
    var elementPartOfTheDom = $('#foo'),
        newElementToAdd = $('<div>If you ever have a question, </div>');

    newElementToAdd.addClass('newElement').append('<a href="http://www.google.com">GOOGLE IT!</a>');

    elementPartOfTheDom.append(newElementToAdd);


    //Caching DOM Element Selections For Multiple Changes (see fiddle)
    //Good: store the selected DOM element so it doesn't have to be located again for subsequent calls
    var domElem = $('#foo');
    domElem.addClass('newElement');
    if ((new Date()).getSeconds() % 2) {
        domElem.css('background-color', 'red').html('ODD SECOND');
    }
    else {
        domElem.css('background-color', 'yellow').html('EVEN SECOND');
    }
    if (domElem.width() > 150)
        domElem.width('300px');

Going Forward

There are many ways to adjust our JavaScript code to make it more performance-oriented, however, here we have defined several ways that concern the workings of JavaScript engines. Realize that some of these changes will not be evident in performance gains individually, but rather will be more helpful when applied to an entire project. Whenever we understand some of the inner workings of JavaScript engines, we are able to produce code that will be fast and efficient.

References

Posted in: JavaScript

Topics: JavaScript Coding Techniques Performance Custom Software Code

Comments