Backbone Events vs. Ember Bindings: A Benchmark

Animating 100 (or N) circles with your standard Backbone events and Ember bindings.

HTML

<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="http://backbonejs.org/backbone-min.js"></script>
<script src="http://cloud.github.com/downloads/emberjs/ember.js/ember-0.9.4.min.js"></script>
<script src="http://cloud.github.com/downloads/SteveSanderson/knockout/knockout-2.0.0.js"></script>

<p>
    I've updated <a href="http://jsfiddle.net/jashkenas/CGSd5/">the original Backbone/Ember test</a> to add a KnockoutJS benchmark using the new native template engine.  All the same caveats apply - this is an arbitrary test.  Also, I'm not all that sure I chose the best way of doing this with Knockout, so if someone can point out a faster way, that'd be great.</p>

<p><strong>Also, I think I stuffed up the init code, such that the test only works right the FIRST time you run it without reloading.  Sorry.</strong></p>

<button onclick="runBackbone()">Animate with Backbone</button>
<button onclick="runEmber()">Animate with Ember</button>
<button onclick="runKnockout()">Animate with Knockout</button>

<div id="grid"></div>

<script type="x-template" id="underscore-template">
  <div class="box" id="box-<%= number %>" style="top: <%= top %>px; left: <%= left %>px; background: rgb(0,0,<%= color %>);">
    <%= content %>       
  </div>
</script>
        
<script type="text/x-handlebars" id="handlebars-template" data-template-name="box">
    <div class="box" {{bindAttr id="model.number" style="model.style"}}>
      {{ model.content }}       
    </div>
</script>

<script type="text/x-template" id="knockout-template">
    <div data-bind="foreach: boxes">
        <div class="box-view">
            <div class="box" data-bind="style: {top: top(), left: left(), background: color()}, text: content()"></div>
        </div>
    </div>
</script>

CSS

p {
  font: 12px/16px Arial;
  margin: 10px 10px 15px;    
}

button {
  font: bold 14px/14px Arial;  
  margin-left: 10px;
}

#grid {
  margin: 10px;   
}

.box-view {
  width: 20px; height: 20px;
  float: left;
  position: relative;
  margin: 8px;    
}

.box {
  border-radius: 100px;
  width: 20px; height: 10px;
  padding: 5px 0;
  color: #fff;
  font: 10px/10px Arial;
  text-align: center;
  position: absolute;
}

JavaScript

// Change N to change the number of drawn circles.

var N = 100;

// The Backbone implementation:
(function(){
    
var Box = Backbone.Model.extend({

    defaults: {
        top: 0,
        left: 0,
        color: 0,
        content: 0
    },
    
    initialize: function() {
        this.count = 0;
    },

    tick: function() {
        var count = this.count += 1;
        this.set({
            top: Math.sin(count / 10) * 10,
            left: Math.cos(count / 10) * 10,
            color: (count) % 255,
            content: count % 100
        });
    }        

});


var BoxView = Backbone.View.extend({
    
    className: 'box-view',
    
    template: _.template($('#underscore-template').html()),
   
    initialize: function() {
        this.model.bind('change', this.render, this);
    },
    
    render: function() {
        this.$el.html(this.template(this.model.attributes));
        return this;
    }
    
});

var boxes;

var backboneInit = function() {
    boxes = _.map(_.range(N), function(i) {
        var box = new Box({number: i});
        var view = new BoxView({model: box});
        $('#grid').append(view.render().el);
        return box;
    });
};

var backboneAnimate = function() {
    for (var i = 0, l = boxes.length; i < l; i++) {
      boxes[i].tick();   
    }
    window.timeout = _.defer(backboneAnimate);
};

window.runBackbone = function() {
  reset();
  backboneInit();
  backboneAnimate();    
};

})();

// The Knockout implementation:
(function(){

var Box = function() {
    var that = this;
    that.count = ko.observable(0);
    that.top = ko.computed(function(){ return (Math.sin(that.count() / 10) * 10) + 'px'});
    that.left = ko.computed(function(){ return (Math.cos(that.count() / 10) * 10) + 'px'});
    that.color = ko.computed(function() { return 'rgb(0,0,' + (that.count()) % 255 + ')'});
    that.content = ko.computed(function() { return that.count() % 100});
    
    this.tick = function() {
        that.count(that.count() +...