« Back to home

What makes Javascript slow?

This post consolidates some of the most notable frontend performance issues related to Javascript and desktop browsers. For mobile web performance, you can read this article from Sencha.

Is Javascript really slow?

No. To be precise, a programming language neither fast nor slow. It's just a language. What's slow is the interpreter/compiler that the language runs on and the environment it interacts with. Modern Javascript engines are not slow, in fact they're blazing fast and highly optimized compare to other interpreted languages like Python and Ruby. To prove that, let's take the Javascript component out of the browser and see how it does. We can install a Javascript engine like V8 (from Chrome) or SpiderMonkey (from Firefox) directly and run some benchmarking tests. On Mac, the two of them can be trivially installed via Homebrew.

brew install v8
brew install spidermonkey

Let's use V8 as it's the fastest out there at the moment. Here are the results for a test for multiplying two 100x100 matrices:

Python 2.7.3           225ms
Ruby 2.0.0             216ms
Javascript V8          23ms

As you can see, the algorithm, which runs in O(n^3), is much faster in Javascript V8 than Python and Ruby. Now let's take this test and run it on Chrome that has V8 embedded. The result is even more surprising:

Chrome 31.0 (V8 3.21)  11ms

So it looks like V8, which is already very fast, is even more optimized to run on the browser. There are some more comprehensive benchmarking tests that confirm the speed of Javascript. You can take a look here or here.

So Javascript is fast, but why are developers still complaining about its performance?

The number one culprit: The Browser(s)

Javascript is just one part of the browser. There are still two more components that make web applications work: markup and CSS. Let's take a look at each of them:

HTML and the DOM

This is the source of all evil. DOM operations are expensive, for example, let's take a look at this code which creates 5000 DOM elements and adds it to a blank page:

for (var i = 0; i <= 5000; i++) {
  var add = document.createElement('div');
  add.innerHTML = 'Item ' + i;
  document.body.appendChild(add);
}

It has roughly 200 times less operations than multiplying 100x100 matrix but takes 53ms, almost 5 times slower, on Chrome 31.0 with V8 3.21. On older browsers, it's even much worse, especially IE 6-8. So Javascript isn't who to blame here. It's the DOM.

A big problem with the code above is that any changes to the DOM causes repaint and reflow. That means the browser has to re-render the part of the page affected by the DOM changes. As you might expect, this is expensive and should be avoided as much as possible. A general rule of thumb is to minimize DOM transactions, i.e. don't touch the DOM unless you absolutely have to. A good technique is using DocumentFragment to batch appending multiple DOM elements to the page.

var fragment = document.createDocumentFragment();

for (var i = 0; i <= 5000; i++) { 
  var add = document.createElement('div');
  add.innerHTML = 'Item ' + i;
  fragment.appendChild(add);
}

document.body.appendChild(fragment);

This is treated as only one transaction by the DOM API, and therefore results in only one reflow. However, modern browsers already optimize for this to make our lives a lot easier. But it doesn't matter if our users are still stuck with browser versions from a couple of years ago.

CSS

How exactly does CSS work? CSS is simply a style sheet that the browser consults before rendering the DOM on the web page. The key thing to note here is that CSS is consulted after the DOM has been generated. That means it also involves DOM traversal and sometimes can cause performance problems.

Contrary to popular belief, the browser reads CSS rules from right to left, not left to right. For example this rule:

treehead treerow treecell .odd {…}

is read as: look for all elements with class odd, then traverse up the DOM tree, filter out the ones not belong to treecell, and then up to treerow and treehead. For the reason why browsers do that, see this SO answer.

Let's do a quick measurement to see how bad this CSS descendant selector actually is. We can use Chrome's Speed Tracer for this purpose. The result below are obtained from SpeedTracer for a document with 100 divs element with the class odd, only a fraction of which match the desired selector.

img

And here is the one for the same document but all the desired elements share a custom desired class. The selector is applied to the desired class directly:

It's almost 3 times improvement in style recalculation time. To be fair, most of the time we don't need to care about CSS performance as modern browsers optimize it quite well (the two versions above are not much different in the latest versions of Chrome). But it's always good to follow the good practices especially avoiding descendant and child selectors. Also it's good to run your application through SpeedTracer to identify performance issues early on.

Javascript: The slow parts

As we already know, Javascript is a relatively fast scripting language. Most of the frontend performance problems are caused by the DOM and browser interaction, not the language itself. However, there are features in Javascript that might be problematic if used incorrectly. Below are some of the notable ones:

Prototyping inheritance

Looking up variables in long prototype chains is not a good thing, especially when it's repeated over and over again. So if you find yourself accessing inherited data frequently, it's better to cache the data in local variables.

Namespace.object.data_1.data_2.data_3.name = "Ryan";

var doSomething = function() {
  // Caching
  var name = Namespace.object.data_1.data_2.data_3.name;

  for (var i = 0; i < 100; i++) {
    console.log(name);
    // console.log(Namespace.object.data_1.data_2.data_3.name); // bad
  }
}

Function scope

Similar to protyping inheritance, looking up data in long function scope chains can also be costly. Again, caching is the key here:

var func1 = function() {
  var name = "Ryan";

  var func2 = function() {
    var nameCache = name;  // Caching

    for (var i = 0; i < 100; i++) {
      console.log(nameCache);
    }
  }
}

Loops

for…in and forEach loops are quite poor in performance compare to the normal for loop. I personally think for…in should be avoided most of the time as it doesn't provide much benefit. forEach should only be used when you need to make use of the function callback it provides. For most of the time, the good old for loop is sufficient. Also, caching array length can provide some more performance gain:

var length = arr.length;

for (var i = 0; i < length; i++) {
     // Do something
   }

Single threaded

The biggest disadvantage of Javascript is the lack of multi-threaded support. That means heavy computation cannot be split up into concurrent tasks to make it faster. There's nothing developers can do about it, and it's pretty much unneccessary anyways. Frontend development doesn't have to deal with IO, which is the most expensive operation in the concurrent programming world. I've also never run into any algorithmic computation too heavy to be required to split up into multi threads.

Note that optimizing the above Javascript features doesn't provide much performance gain compare to optimizing DOM manipulation. Unless you're working with very old browsers, this shouldn't be much of a concern.

Conclusion

Javascript is probably the most misunderstood language in the world. It's a fast scripting language, much faster than Ruby or Python, but the browser has given Javascript such a bad image. The DOM is slow, and Javascript has done the best it could to offset the many issues with DOM manipulation. The most important thing to take away for frontend developers is to know where and when to touch the DOM and question every DOM manipulation. Also it's crucial to always know your users and the browsers they're using. There's no point in optimizing for performance when the users' browsers already do it for you. For more in depth views on frontend performance optimization, please check out the references below.

References

Nicholas C. Zakas: Speed Up Your Javascript

Ariya Hidayat and Jarred Nicholls: Hacking WebKit & Its JavaScript Engines

Steve Souders: High Performance Websites

Sencha: 5 Myths About Mobile Web Performance

Google Developers: SpeedTracer Examples

Google Developers: Web Performance Best Practices

Mozilla: Writing efficient CSS

Comments

comments powered by Disqus