JavaScript Performance Tips & Tricks

Grgur Grisogono

July 31, 2012

Some say spending time developing for performance is not worth it when hardware upgrades are usually a cheaper alternative. If I would tell them that spending 10 minutes reading this could save more than 50 new upgrades with simple code improvements that account for a 50x+ performance increase, do you think they would listen?

From rarely used and almost forbidden code snippets to commonly used methods and loops, I am about to show how to cut unnecessary milliseconds out of your JavaScript application.

All tests are measured in ops/sec (higher is better), the widely known jsperf.com benchmarking style. In fact, benchmark.js was used to conduct the tests cross-browser. The tests were conducted on a 2011 MacBook Pro, 2.2 GHz i7 processor with 8GB RAM and a Vertex 3 SSD, all running on a 10.7.4 OS X Lion or Windows XP SP3 through Parallels 7.

Evaluation

I can hear you saying “Oh but Crockford says eval is evil and I agree with him.” I concur, but there are cases when evaluation is important, such as:

  • Code caching. Sometimes we choose to cache parts of our apps in the browser through one of the available web storage mechanisms.
  • Deferred execution. On mobile devices, JavaScript execution takes approximately 1ms for each 1kB of code.
  • Loading on demand. Some architects choose to load application modules on demand to save on bandwidth and increase security measures.

eval() is the function available in JavaScript that is meant to be used for code evaluation. It accepts a string that should represent a section of code, properly formatted.

Let’s examine the following code:

var str = "",
    i = 0;
for (; i<1000; i += 1) {
    str += "a";
}

This simple loop appends a character to a string. Strings are immutable in JavaScript, thus str will be overwritten with each change. Now, let’s change the code into a eval-compatible string.

var strCode = 'var str="",i=0;for(;i<1000;i+=1){str+="a";}';

Ugly. Benchmarking eval(strCode) on Chrome 21 will output 97 ops/sec. Make sure you make a mental note here.

Alternatively, let’s do the same thing, but this time we will evaluate using a little trick:

(new Function(strCode))()

Benchmarking this snippet in the same browser will output 5,256 ops/sec, a staggering 54.2x (or 5,418%) speed increase.

JavaScript Evaluation vs (new Function())()

Google Chrome’s V8 engine is to ‘blame’ for such performance. Unfortunately or luckily, depending on how you look at it, not all browsers use V8. However, all browsers consistently report performance benefits with the latter approach, whether they are by as much as 5400% or 10% (IE6).

String Concatenation

This section is unique because we often use concatenation techniques for code readability and organisation benefits in addition to business logic purposes. In other words, we tend to intentionally sacrifice application performance in favour of prettier code. I’ll stick to this notion to demonstrate the speed differences. Common concatenation principles include:

foo = foo + bar;
foo += bar;

The first one will turn out to be faster in all browsers, by a small margin (only Safari 5 will double the performance). That’s good to know, sure, but there’s more to come.

How many times have you seen something like this bit of code:

Ext.create('MyView', {
    tpl: [
        '<div class="heading">',
            '<div class="firstname">',
                '{firstname}',
            '</div>',
            '<div class="lastname">',
                '{lastname}',
            '</div>',
        '</div>'
    ].join()
});

I created an array of strings and used Array.prototype.join to combine them into one. Of course, it’s much prettier than combining with a + sign, and the code is more readable, too.

[].join() is, naturally, slower in all browsers but Chrome. Just as the evaluation test above, Chrome is able to employ advanced caching techniques to deliver faster results for repeated actions. In other words, in real life [].join() will always be slower.

Let’s rewrite that statement, persist the prettiness, and increase the performance:

Ext.create('MyView', {
    tpl: ''.concat(
        '<div class="heading">',
            '<div class="firstname">',
                '{firstname}',
            '</div>',
            '<div class="lastname">',
                '{lastname}',
            '</div>',
        '</div>'
    )
});

Instead of creating a new array, filling it up with strings and then joining it, I simply created an empty string and used String.prototype.concat method to append arguments to it. Visually it doesn’t make a huge difference, the code is as pretty as it was. However, the latter performs significantly faster than any other form of concatenation.

JavaScript String Concatenation Chart

Look at the ''.concat() bars skyrocketing so much that [].join() looks incredibly silly. In fact, the benefit is exponential across browsers.

Update:  After further examination, String.prototype.concat is faster with lower number of arguments (especially faster with just one argument). It is also faster in general in OSX Safari and iOS Safari. On the other hand, Array.prototype.join gains speed with more array items (string particles to concatenate) in play. Even after two strings, join becomes significantly faster. The benchmark above is done, as suggested in the legend, with a single string to append (argument, array item) in all cases.

Loops

When simple iterations are required, in other words repeating an action n times, we will often use a while instead of a for loop.

for (var i = 0; i<1000; i++) {
    // do your stuff
}

vs

var i = 1000;
while (i--) {
    // do your stuff
}

Anyone who has to deal with (read: support) Internet Explorers will say that the while loop is faster. That is true for all IE9 and older. New browsers, however, cancel this out in favour of the for loop. Yet, the performance increase in percentage is higher than the one found in IEs.

For Loop vs While Loop - JavaScript Performance

Accessing Object Properties

Repeated access to nested object properties is, logically, going to introduce performance drawbacks. Here is what I mean:

for (var i in App.view.tablet.Viewport) {
    console.log(App.view.tablet.Viewport[i]);
}

Let’s dissect the line inside the for loop. I am telling browser to access App object, then find view reference and access it, then find tablet and open it to access Viewport. That’s four references so far plus the last one of the value of i. To reach each reference our JavaScript code communicates with the browser, the browser communicates through it’s internal components to the OS and finally to reach the desired memory allocation in RAM. And we do this five times.

Instead, let’s cache the static part, or the first four steps and see what it brings us:

var vp = Ext.view.tablet.Viewport;
for (var i in vp) {
    console.log(vp[i]);
}

Not only that the code is shorter in total number of bytes, but it’s faster.

Nested Object Properties

Reusing Array References

Often in our code we work with temporary references that get discarded in time. Arrays in particular can be reused, or should we say recycled, thus saving some of the precious processing time. This is how it’s done:

var foo = [1,2,3];

// do something with foo, then reuse it to fill it with new values

foo.length = 0;
foo.push(5, 6, 7);

The advantage over creating a brand new array instance can be interesting, at least in some browsers.

Reusing Arrays for JavaScript Performance

Interestingly, Chrome is ridiculously fast with re-creating a new array, and it actually lost a great deal with the suggested approach. All other browsers, however, benefited greatly.

Optimising Events

JavaScript’s event-driven nature is one of the major strengths of the language. At the same time, having an enormous number of events can be sub-optimal and degrade application performance significantly. Imagine 1000 nodes each listening for an event, then testing for each one of them in capturing and bubbling phase of event handling. Expensive. Here is what I mean:

<ul id="menu">
    <li id="home">Home</li>
    <li id="products">Products</li>
    <li id="portfolio">Portfolio</li>
    <li id="shop">Shop</li>
    <li id="about">About</li>
    <li id="login">Log in</li>
    <li id="contacts">Contacts</li>
</ul>
document.getElementById('top').addEventListener('click',goHome);
document.getElementById('products').addEventListener('click',goProducts);
// ...
document.getElementById('contacts').addEventListener('click',goContacts);

It this example, every list item is assigned an event listener. That gives us 7 event listeners in total, which in effect becomes difficult to manage, consumes more code, and more work for both developer and the interpreter.

In contrast, we could have done this:

var menuHandler = function(event) {
    event = event || window.event;
    var target = event.target || event.srcElement;
    if (target.id === 'home') {
        // go home
    }
    // else ...

}
document.getElementById('menu').addEventListener('click',menuHandler);

Here we assigned a single event listener that acts as a delegate for target element’s child nodes. It much more effective, especially when the list of child nodes is big.

Event delegation is a very common need with Ext.Templates (or XTemplates) in Sencha Touch and Ext JS. It’s also simple to use with built-in helpers. Let’s have a quick look:

Ext.createWidget('panel', {
    data: [
        {id: 'home',      name: 'Home',      url: 'index.html'},
        {id: 'products',  name: 'Products',  url: 'products.html'},
        {id: 'portfolio', name: 'Portfolio', url: 'portfolio.html'},
        {id: 'shop',      name: 'Shop',      url: 'shop.html'},
        {id: 'about',     name: 'About',     url: 'about.html'},
        {id: 'login',     name: 'Login',     url: 'login.html'},
        {id: 'contacts',  name: 'Contacts',  url: 'contacts.html'}
    ],
    tpl: ''.concat(
        '<ul id="menu">',
            '<tpl for=".">',
                '<li id="{id}">{name}</li>',
            '</tpl>',
        '</ul>'
    ),
    listeners: {
        element: 'body',
        click: function(event, element) {
            // element represents the clicked-on list item
            console.log(element);
        }
    }
});

In this example, the menu is rendered from a data set using Ext.XTemplate. A listener for click event is assigned to panel’s body element. The handler attached to the event listener works with the element argument, which references the LI element user clicked on.

This approach also gives more power to the developer who can access the panel component directly with the this keyword when using proper scoping.

Conclusion

In this document, we went over some of the commonly used snippets and demonstrated how they can be improved to yield faster applications. This is especially important for mobile web applications and sites as the numbers of mobile clients has been on a significant rise (and will continue to do so), while mobile devices are not as powerful as desktops, hardware-wise.

Many of the optimisations recommended here also offer different tactics for different browsers. In other words, being browser aware will certainly help deliver top notch web application performance.

I suggest you take a look at Efficient DOM and CSS Operations post that discusses performance with HTML elements.

Do you have your own findings on the topics presented? Do you have additional suggestions to show? Please share your thoughts with the rest of us.


  • Mike

    array.join() was significantly faster in previous versions of Rhino

  • Les Szklanny

     
    Look at the ”.concat() bars skyrocketing so much that [].join() looks incredibly silly.

    In IE6 and IE7 [].join is faster.

    http://www.amazon.com/Performance-JavaScript-Faster-Application-Interfaces/dp/059680279X/ref=sr_1_2?ie=UTF8&qid=1343753256&sr=8-2&keywords=javascript+performance 

  • Les Szklanny

    I’d suggest using new Function in Ext.JSON.decode, which by default still uses eval… silly.

    Even jQuery is using new Function:

    http://stackoverflow.com/questions/2449220/jquery-uses-new-functionreturn-data-instead-of-evaldata-to-parse 

    http://forum.jquery.com/topic/json-performance-comparison-of-eval-and-new-function 

  • Jonathan

    Thanks for you article, I am sure it will help a lot of people on how get the much ouf of them code and browser.
    JB

  • grgur

    @e636ecbf26fe7c854c255b3a3dabe7b9:disqus  @5d92488d6c2ec1ef870ac0d1efbc74d4:disqus Yes, I agree things used to be different. Fortunately, the evolution we have been facing in the last couple of years is exhilarating enough to bring up a bit fresher benchmarks :)
    Ext.JSON.decode utilizes JSON.parse if it exists, then falls back to eval(). Good point tho, I’m sure this will get updated, knowing the high performance standards the Sencha team has been following

  • http://twitter.com/jeremypetrequin Jérémy Petrequin

    Great article! Maybe a little mistake in the delegate good example, it’s not document.getElementById(‘contacts’).addEventListener(‘click’, menuHandler); but 
    document.getElementById(‘menu’).addEventListener(‘click’, menuHandler);  I think?

  • grgur

    @twitter-79966394:disqus Good catch, thanks! Fixed

  • Les Szklanny

    This is what we need in Ext (adapted from Google Closure API):

    Ext.String.buildldString = function(var_args) {  return Array.prototype.join.call(arguments, ”);};Ext.String.buildldString(‘a’, ‘b’, ‘c’, ‘d’);

  • Grgur

    I posted an update on join vs concat. Their comparison changes with number of arguments or array items. To make it more interesting, not all browsers react the same – concat is always faster in Safari, while join is tremendously faster in Chrome with more string particles to concatenate. 

  • vadimv

    thx, nice to know about ‘concat’, especially for XTemplates

  • vadimv

    ha, just saw the comment regarding your update, would be great to know if join with more arguments is also fast on FF and IE !!!.

    • Grgur

      You will probably be best off using concat in Sencha Touch 2 (esp on iOS only projects) and join in Ext JS. Using it in XTemplates is a form of micro-optimization, anyway. 

  • http://mrchief.myopenid.com/ mrchief

    It’s interesting how you present “deceptive” benchmarks – you show concat
    with multiple arguments but then admit that the benchmarks were done using a single
    argument and then even go on to admit that join is significantly faster. I
    appreciate your honesty but at
    the same time, I don’t trust any of thebenchmarks anymore. You could have provided
    jsperf links so we could all see and know for sure.

     

    It’s also disturbing how Chrome stands out like a deranged browser
    having all this crazy optimizations which makes implementing any optimizations
    thru code extra hard cause while others browsers may benefit, chrome may suffer
    – so now you need to have browser specific optimizations, or choose if you want
    to leave chrome behind or other browsers behind, or not do anything at all!

    • Grgur

      Thanks for your elaborative comment. I really appreciate responses like yours. 

      The concat ‘issue’ is a great example of how benchmarks can be as misleading, just as any other statistical sample. I too do not trust benchmarks fully, as they only represent a single use case point of view. The very concat example offered with multiple arguments still stands true for some browsers, namely desktop and mobile versions of Safari. Tuning for performance takes a great amount of work, just as benchmarking needs observation for multiple perspectives. After all, many libraries and framework approach computations with different solutions based on the browser environment to provide with results in the fastest manner possible. All benchmarks demonstrated are built with benchmark.js, and the code used to benchmark is either included in the code boxes or in chart legends. You can easily extract them out and test yourself. Should there be a greater demand for it, I will gladly create respective jsperf projects and link them here. 

  • Pingback: Efficient DOM and CSS Operations | Modus Create

  • Pingback: NoVaJS Meetup : JavaScript Performance Tips with Grgur Grisogono | Modus Create

  • http://www.facebook.com/people/Austin-Louis/100000881457662 Austin Louis

    Wondering if it would be appropriate to do the following to overwrite eval?

    function eval(strCode){
    (new Function(strCode))();
    }

    Thanks
    Austin

    • Grgur

      Good questions. However, overwriting native methods is never a good idea. I wouldn’t recommend it.

    • http://www.charly-studio.com/ Charles HETIER

      Hie, very interesting article
      Depending on code you want to execute, there could have issues in replacing eval with function because of the scope in which the code will be executed in the last case.

  • http://jquery4u.com/ jQuery4u

    Nice article mate.

  • http://kendru.github.io/ Andrew S. Meredith

    Awesome article. Definitely worth the 10 minutes!

  • Cal Leeming (sleepycal/foxx)

    Really awesome article, quite fascinating the difference between eval() and Function(strCode). Thanks for sharing!


What We Do

We’ll work closely with your team to instill Lean practices for ideation, strategy, design and delivery — practices that are adaptable to every part of your business.

See what Modus can do for you.

We're Hiring!

Join our awesome team of dedicated engineers.

Loading...