Finding JavaScript Bottlenecks

By: Keith Clark

Tags:

  • javascript
  • performance

We’re currently auditing the performance of the WLD platform. I’ve been looking at the front-end performance, particularly how JavaScript can be improved.

Profiling JavaScript

The best way to monitor JavaScript performance is to use the profiling tool built into most modern web browsers. A profiling tool will record the execution order and timing of individual JavaScript functions and generate a report outlining how the page performs. Profiling tools are invaluable when it comes to tracking down the bottlenecks in JavaScript code.

Chrome, Safari, Firefox (via Firebug) and Internet Explorer 9 have tools built in for profiling JavaScript. For this post I’ll be using Chrome but it’s important to profile your scripts in more than one browser.

Chrome’s JavaScript profiler can be found in the Developer Tools, under the Profiles tab. Using it to record activity is simple: clicking “Start” before loading a page begins the recording and clicking “Stop” once the page has finished loading ends it. Once a profile has been created you’re ready to analyse the generated report.

Chrome's Profiler

The “Self” column shows how long a function took to execute and the “Total” column shows how long a function and any nested functions took to execute. Selecting the “Top down” view and sorting the results by the “Total” column will give us the function call stack, listing the most expensive functions first.

The first item in the report can be confusing because it appears to represent the total execution time of the JavaScript on the page, but that’s not really the case it actually shows how long the current profile was recording.

The second item in the report shows the slowest code path, taking a total of 1.61 seconds to complete! We need to find out what’s taking so long.

The “Self” column shows that the function in question actually took no time to execute which rules it out as the culprit so it must be calling a slow function deeper in the call stack – we need to dig a little deeper to see what’s causing the problem.

Expanding the row reveals the functions that were directly called by the current function. The profiler view can become messy as you drill down through the function stack, you can make it a little easier on the eye by filtering out everything but the function you’re interested in by clicking the focus icon (the eye).

Chrome's Profiler

Following the same process as above, we can see that two functions klass and SearchLocationToggle.init were called. SearchLocationToggle.init took no time to run and its child functions took no more than 1ms so we can rule this out, leaving us with “klass”.

We keep drilling down through the function calls until we see a high value in the “Self” column…

Chrome's Profiler

A function called stripScripts is taking 710ms to execute – let’s start there.

Next to each function is a link to its definition. The link points to either prototype.js, a 3rd party JS library or fancy_selects.js, an enhanced JavaScript replacement for the <select> dropdown menu. It’s tempting to jump straight into the prototype.js file and look at making stripScripts run faster but we need to look at our own code first.

Working back from stripScripts we can see that $$.each.grouped.each.list was the function in fancy_selects.js that resulted in the function being called. Clicking the link reveals the following code:

list = new Element('ul').addClassName(this.options.listClassName);

group.each(function(option, index) {
  var text          = option.innerHTML;
  var value         = option.readAttribute('value');
  var itemClassName = 'item '+ (index + 1) +'_item ';
  var name          = element.readAttribute('name');
  var id            = element.readAttribute('name') + '_' + value;

  itemClassName += (index == (group.size() - 1) ? ' last' : '');

  if (option.readAttribute('selected') == true || option.readAttribute('selected') == 'selected') {
    itemClassName += ' selected';
    setValue(value);
    setText(text);
  }

  list.insert(
    new Element('li').addClassName(itemClassName).insert(
      new Element('label')
        .observe('click', bindSelectOption.bindAsEventListener(this))
        .observe('wld:click', bindSelectOption.bindAsEventListener(this))
        .insert(text)
    ).insert(
      new Element('input', {type: 'hidden', value: value}).addClassName('fancy_hidden_value ignore')
    )
    .observe('mouseover', function(e) { Event.element(e).addClassName('hover'); })
    .observe('mouseout', function(e) { Event.element(e).removeClassName('hover'); })
  );
}.bind(this));

This block of script recreates a <select> menu option list using <ul>, <li>, <label> and <input> tags. It also binds event handlers for managing selection and hover states. If we switch back to the profile output we can see that stripScripts is called by Element.methods.insert (Element.insert), which is called 4 times in the above code.

A quick review of the page shows that this method is run on 7 different <select> elements, containing 502 options which, when multiplied by the number of generated HTML elements, comes to 2008 elements and that doesn’t include text nodes used for holding the item values.

Fixing the problem

This method of creating a selectable list is very inefficient. There’s no need to create so many element objects to produce a static option list. It would be much faster to create a string of HTML and leverage event delegation to handle the user interaction.

We completely rewrote the dropdown control to improve both speed and accessibility. During the development performance profiles were run periodically to ensure we were making the correct decisions regarding speed. As you can see from the screenshot below the same work as above is achieved in around 50ms – a massive improvement.

Chrome's Profiler


About the Author

Keith Clark