function Class1() {
// "public" properties
this.name = joe";
// "private" properties (works through closures)
var _age = 22;
// method
this.getAge = function() {
return this._age;
}
}
Class1.prototype = new BaseClass();
For Prototype, you might write:
Class2 = Class.create();
Class2.prototype = Object.extend(new BaseClass(), {
name: "fred",
initialize: function() {
this._age = 22;
},
getAge: function() {
return this._age;
}
});
These two approaches are similar, with two main distinctions:
In general, the Atlas idiom places more information directly on this,
whereas Prototype makes more use of the JavaScript prototype chain.
Atlas makes use of JavaScript closures for private instance properties. Prototype classes generally don't do this.
Its hard to compare these two idioms directly. After all, both are legal JavaScript, so coders can (and do) mix-and-match, adopting different idioms on a per-class or even per-method basis.
I also believe that no performance metric can tell you which technology is right for a particular job. Perf tests are just tables of numbers, nothing more. That said, it is important to understand the performance characteristics of the technology you are using.
Keeping that in mind, lets do some measurements. The source code for these measurements is here.
We start with BaseBaseClass:
function BaseBaseClass() { }
BaseBaseClass.prototype.p0 = 10;
BaseBaseClass.prototype.p1 = 20;
// inherited method 0
BaseBaseClass.prototype.testmethod0 = function() {
return this.p0 + this.p1;
}
and then define BaseClass:
function BaseClass() { }
BaseClass.prototype = new BaseBaseClass();
BaseClass.prototype.p2 = 10;
BaseClass.prototype.p3 = 20;
// inherited method 1
BaseClass.prototype.testmethod1 = function() {
return this.p2 + this.p3;
}
BaseClass and BaseBaseClass are very similar - they both define methods that add two instance properties. BaseClass is derived from BaseBaseClass.
We then define two subclasses, Class1 and Class2. Both derive from BaseClass. Class1 uses the Atlas idiom. Class2 uses the Prototype idiom:
/* Atlas Style Class */
function Class1() {
...
}
Class1.prototype = new BaseClass();
/* Prototype Style Class */
Class2 = Class.create();
Class2.prototype = Object.extend(new BaseClass(), {
...
});
To run the tests, we make an instance of Class1 (Atlas) and Class2 (Prototype) and call testmethod0 and testmethod1 a bunch.
| Test 0 - BaseBase method | Atlas idiom | Prototype idiom |
| IE6 | 641 | 641 |
| FireFox | 180 | 180 |
| Opera | 150 | 140 |
| FireFox/Mac | 197 | 227 |
| Safari | 1575 | 1587 |
| Test 1 - Base method | Atlas idiom | Prototype idiom |
| IE6 | 481 | 480 |
| FireFox | 171 | 170 |
| Opera | 140 | 150 |
| FireFox/Mac | 262 | 200 |
| Safari | 1541 | 1568 |
[Numbers are elapsed milliseconds for 100,000 iterations. The first three rows are for a 1.6Ghz Pentium 4 running Windows XP Service Pack 2, FireFox 1.0, Opera 8.51; the last two for a 1.66Ghz PowerBook G4 Mac running OSX Tiger, FireFox 1.0. The absolute figures are not that important - we are interested in relative performance.]
The Atlas and Prototype idioms perform roughly the same here - both are inheriting from BaseClass in the same way.
On IE, Test 1 runs 30% faster than Test 0. With FireFox/Safari the difference is around 5% (not significant). The conclusion: on IE, accessing properties/methods that are further up the prototype chain is significantly slower than accessing properties that are "closer". You pay a penality for calling methods or getting properties defined on superclasses. With FireFox/Safari, the penalty is much less.
On the PC, the code runs about 3 times faster in FireFox than JavaScript, on the Mac FireFox is about 8 times faster than Safari, Opera is the fastest (on all tests) - noteworthy differences that I won't focus on here.
For this test, we compare this method using the Atlas idiom:
function Class1() {
this.testmethod2 = function() {
return this.p2 + this.p3;
}
...
with this from the Prototype idiom:
Class2.prototype = Object.extend(new BaseClass(), {
testmethod2: function() {
return this.p2 + this.p3;
},
...
The body of the method is identical to Test1 (both are adding the p2 & p3 properties defined by BaseClass).
Note that Atlas and Prototype versions differ in where the method is stored. With
Atlas, the method is set directly on the this instance. With Prototype, the method ends up in this.prototype.
| Test 2 - this vs this.prototype | Atlas idiom | Prototype idiom |
| IE6 | 401 | 450 |
| FireFox | 160 | 160 |
| Opera | 130 | 140 |
| FireFox/Mac | 147 | 179 |
| Safari | 1551 | 1576 |
On all browsers, Test 2 runs faster than Test 1 - around 15% faster
on IE6, and on FireFox/Mac. In other words, a method set on this
or this.prototype is faster to invoke than one defined
in a base class, further up the prototype chain.
On IE, and on Safari/FireFox on the Mac, the Atlas idiom is marginally faster than the Prototype idiom. Invoking a method set directly on this is up to 10% faster than calling a method set on this.prototype.
This test is similar to Test 2, except this time we access properties
stored directly on this.
In the Atlas idiom, we use:
// "public" properties
this.p4 = 30;
this.p5 = 40;
// add properties set directly on the object
this.testmethod3 = function() {
return this.p4 + this.p5;
}
For prototype, we use:
// public properties
initialize: function() {
this.p4 = 10;
this.p5 = 20;
},
// add properties set directly on the object
testmethod3: function() {
return this.p4 + this.p5;
},
The code is very similar to Test 2 - the only difference is that the
two properties being added are set directly on this.
| Test 3 - Properties on this | Atlas idiom | Prototype idiom |
| IE6 | 290 | 310 |
| FireFox | 130 | 130 |
| Opera | 100 | 120 |
| FireFox/Mac | 108 | 134 |
| Safari | 1502 | 1525 |
On all browsers, Test 3 runs faster than Test 2 - about 15% faster on IE, 40% faster on FireFox/Mac. This is consistent with our earlier observation that things that are nearer on the prototype chain can be accessed more quickly.
As with test 2, the Atlas approach is marginally faster than the Prototype approach, but only by a few percentage points.
Test 4 compares this code using the Atlas idiom:
function Class1 {
...
// "private" properties (works through closures)
var p6 = 10;
var p7 = 20;
// properties from the closure
this.testmethod4 = function() {
return p6 + p7;
}
...
With this Prototype-style code:
Class2.prototype = Object.extend(new BaseClass(), {
...
// Set properties on prototype
p6: 30,
p7: 40,
// properties from this prototype
testmethod4: function() {
return this.p6 + this.p7;
},
...
The difference here is that Atlas is using a closure for private instance variables. The Prototype code is using properties set on the prototype.
| Test 4 - Closures | Atlas idiom | Prototype idiom |
| IE6 | 280 | 360 |
| FireFox | 130 | 150 |
| Opera | 100 | 130 |
| FireFox/Mac | 124 | 166 |
| Safari | 1538 | 1542 |
On both PC browsers, the Atlas closure version runs at the same speed as Test 3. On the Mac, closures are a small fraction slower. In general, using closures instead of properties set on this is not a big speed penalty.
Accessing a closure is faster than accessing a property stored on
this.prototype - on IE6 by 15%, on FireFox/Mac by as much as 30%.
For this test, we simply construct 10,000 instances of Class1 and Class2.
| Test 5 - Constructors | Atlas idiom | Prototype idiom |
| IE6 | 220 | 150 |
| FireFox | 1070 | 530 |
| Opera | 140 | 60 |
| FireFox/Mac | 367 | 254 |
| Safari | 1266 | 506 |
Here you see a major downside of the Atlas idiom. Storing information
directly on this makes constructors take longer (and also makes instances larger) -
the constructor for Class1 takes roughly twice as long as Class2 on FireFox/Windows, and more than twice as long on Safari.
The Prototype idiom makes more extensive use of prototypes - less work is done in the constructor. So Prototype constructors run faster on all platforms.
One of the crowning features of Prototype is the Enumerable class, which lends JavaScript code a Ruby-like functional style. Although not part of the class definition idiom, it is a major feature of Prototype, so I'll test it too.
In this test, we compare the old style of iterating over an array:
var b = 0;
for (var i = 0; i < arr.length; i++) {
var x = arr[i];
if (x > 20) break; else b += x;
}
return b;
with Prototype's functional stype:
var b = 0;
arr.each(function(i) { if (i > 20) throw $break; else b += i; });
return b;
I tried to keep the code within the loop body very similar, so the test is measuring the overhead of the each() and $break mechanism.
The test array we use is:
var arr = new Array(1,2,3, ... ,19,20,21,22);
In other words, the loop iterates over a twenty-item array and then does a break.
| Test 6 - Iterators | For loop | Function |
| IE6 | 10 | 90 |
| FireFox | 10 | 240 |
| Opera | 20 | 110 |
| FireFox/Mac | 52 | 173 |
| Safari | 316 | 903 |
(This test run for 1000 iterations)
The functional approach is certainly more elegant and streamlined. However, this elegance comes at a cost. The each/$break functional approach performs dismally - at least six times slower than a basic for loop on IE6, at least 3 times slower on Safari/FireFox on a Mac.
In both approaches, there is a penalty for subclassing, especially on IE.
For a performance-critical class, set frequently accessed properties/methods
directly on the this instance rather than placing them further up the
prototype chain.
The Prototype idiom, generally speaking, produces code that runs marginally slower on IE, when doing general-case method/property access.
The Atlas idiom has the benefit of truly "private" instance variables - you can use closures to define variables that are only visible within a specific method. There appears to be no perf penality for accessing/updating these closure variables.
A major drawback of the Atlas approach is that constructing instances takes much longer. For a class where thousands of instances are constructed (e.g. a IsoDate class), this is a significant issue. For a class that is constructed once and lives the lifetime of the app, this may not be an issue.
The functional iterators of Prototype perform slowly compared to for loops.
Isaac Lin | December 22, 2005 10:53 AM
The savings in construction is applicable for default constructed objects. For cases where the data members are set at construction time, I imagine that with the Prototype approach, you would incur a similar cost as the usual approach (proportional to the number of members that need to be set).
Mark Wubben | December 22, 2005 02:22 PM
Which Safari version did you test on? There have been some significant speed improvements since 2.00 according to the development team.
jonmeyer | December 22, 2005 06:58 PM
It is Safari 2.0.1, OSX 10.4.2 Tiger.
david | December 23, 2005 02:59 AM
I only use the Prototype object.extend (actualy object.prototype.extend) method when simulating multiple inheritence, is this also possible with the Atlas style?
Sebs | December 23, 2005 10:10 AM
The test incorporated call time .. what about mem consumption ?
Interesting anyways, we encourage our devs to store a lot of stuff in prototypes. makes sense, meassuring the ram used gives you an answer i guess.
aReader | December 23, 2005 11:30 AM
Just a quick side note. The stats are great, and much appreciated, but when you render a table of numbers for results, can you switch the font from Georgia to "Courier", "Courier New", or something else?
It makes numbers hard to read, esp. like 367, and 1266 realy annoying.
Thanx.
jonmeyer | December 23, 2005 12:08 PM
I fixed the tables. Sorry.
Sharebear | January 3, 2006 03:14 PM
Any chance you could run these tests in Opera too? Personally I would be very interested in seeing how they compare speed wise (both 8.51 and 9 preview).
Thanks
[done for Opera 8.51 -- jon]
Daniel | February 21, 2006 08:57 AM
Great experiment! :-)
But, is there a good way to make private variables with Prototype?
Daniel
Wonderful experiments. I expected the Atlas style would be faster for many actions as it is using straight ECMA 1 style, but was surprised at the Constructor test.
I tweaked the loop structure of Test 6, letting it self terminate and saw no difference in the numbers, too bad :(
var b = 0;
var count = 20;
while (count--) {
var x = arr[i];
b += x;
}
return 0;
Thanks again!