Angular VS Knockout VS Ember VS React VS Mithril VS Mercury VS Ractive VS Vue VS Riot

JavaScript performance comparison

Revision 590 of this test case created by PhiLho

Preparation code

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.12/angular.min.js"></script>
<script src="http://fb.me/react-0.12.2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script>
<script src="http://builds.emberjs.com/canary/ember-template-compiler.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ember.js/1.11.1/ember.min.js"></script>
<script src="https://rawgit.com/Raynos/mercury/master/dist/mercury.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/0.1.34/mithril.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/0.11.5/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ractive/0.7.2/ractive.js"></script>
<!--
<script src="https://cdnjs.cloudflare.com/ajax/libs/riot/2.0.14/riot.min.js"></script>
-->
<script src="https://cdn.jsdelivr.net/g/riot@2.0.14(riot.min.js+compiler.min.js)"></script>

<!-- Angular -->
<div ng-app="ngApp">
  Angular (using track by $index):
    <div ng-controller="Ctrl" id="angList"><span ng-repeat="item in data track by $index">{{item}}</span></div>
</div>

<!-- Knockout -->
<div id="koapp">
  Knockout:
  <div data-bind="foreach: data"><span data-bind="text: $data"></span></div>
</div>

<!-- Knockout Rate Limit -->
<div id="koappRL">
  Knockout (rate limit):
  <div data-bind="foreach: data"><span data-bind="text: $data"></span></div>
</div>

<!-- Ember -->
<div id="emapp">
  <script type="text/x-handlebars">
    Ember:
    <div>
      {{#each EMapp.data}}<span>{{this}}</span>{{/each}}
    </div>
  </script>
</div>

<!-- React -->
<div id="react">
  React: <div id="reactMountNode"></div>
</div>

<!-- Mithril -->
<div id="mithrilapp">
  Mithril: <div id="mithrilMountNode"></div>
</div>

<!-- Vuejs -->
<div id="vuejs">
  Vuejs: <div id="vuejsMountNode"><span v-repeat="item: data">{{ item }}</span></div></div>

<!-- Mercuryjs -->
<div id="mercuryjs">
  Mercuryjs: <div id="mercuryjsMountNode"></div>
</div>

<!-- Ractive -->
<script id='ractiveTemplate' type='text/ractive'>
  {{#each items}}<span>{{item}}</span>{{/each}}
</script>
<div id="ractive">
  Ractive: <div id="ractiveMountNode"></div>
</div>

<!-- Riot -->
<riot-component></riot-component>
<script id='riotApp' type='riot/tag'>
  <riot-component>
    Riot: <span each={ item, i in items }>{ item }</span>

    this.items = [];

    this.clear = function()
    {
      this.items.splice(0);
//      this.update();
    }
    this.push = function(e)
    {
      this.items.push(e);
//      this.update();
    }
  </riot-component>
</script>


<script>
  window.getId = function(idx)
  {
    var d = Date.now() + "";
    return idx + "-" + d.substring(d.length - 6);
  };

  var timeout = 0;
  var ngApp = angular.module('ngApp',[]);
  ngApp.controller('Ctrl', function ($scope) {
    $scope.data = [];
  });


  $(document).ready(function() {

    angular.element(document).ready(function() {
      var ang_scope = $('#angList').scope();

      window.ANGclear = function(){
        ang_scope.data = [];
      };
      window.ANGpush = function(data){
        ang_scope.data.push(data);
        if (Date.now() > timeout + 16) {
          var id = window.setTimeout(function() {
            ang_scope.$digest();
            window.clearTimeout(id);
            //console.log("triggered digest");
          }, 0);
          timeout = Date.now() + 16;
        }
      };
    });


    var KOData = ko.observableArray();
    var KOviewmodel = {data: KOData};
    ko.applyBindings(KOviewmodel, document.getElementById('koapp'));

    window.KOclear = function (){
      KOData.splice(0);
    };
    window.KOpush = function (data){
      KOData.push(data);
    };


    var KODataRL = ko.observableArray();
    KODataRL.extend({ rateLimit: 0 });
    var KOviewmodelRL = {data: KODataRL};
    ko.applyBindings(KOviewmodelRL, document.getElementById('koappRL'));

    window.KOclearRL = function (){
      KODataRL.splice(0);
    };
    window.KOpushRL = function (data){
      KODataRL.push(data);
    };


    var ENV = {EXTEND_PROTOTYPES: false};

    EMapp = Ember.Application.create({
      rootElement: '#emapp'
    });
    EMapp.data = Ember.A();

    window.EMclear = function () {
      EMapp.data.clear();
    };
    window.EMpush = function (data) {
      EMapp.data.pushObject(data);
    };


    var ReactData = [];
    var ReactUpdate = null;
    var ReactComponent = React.createClass({
      displayName: 'PerfTest',
      getInitialState: function() {
        return { data: ReactData };
      },
      componentDidMount: function() {
        ReactUpdate = this._onData;
      },
      render: function() {
        return React.DOM.span(null,
            this.state.data.map(function(result) {
              return React.DOM.span(null, result);
            })
          );
      },
      _onData: function() {
        this.setState({ data: ReactData });
      }
    });
    var reactComp = ReactComponent();
    React.renderComponent(reactComp, document.getElementById('reactMountNode'));


    window.RClear = function() {
      ReactData.splice(0);
      ReactUpdate();
    };

    window.RPush = function(data) {
      ReactData.push(data);
      ReactUpdate();
    }


    var h = mercury.h
    var mdata = mercury.array([])
    mercury.app(document.getElementById('mercuryjsMountNode'), mdata, mrender)
    function mrender(data) {
      return h("span", data.map(function(datum) { return h('span', datum)}));
    }

    window.mercuryClear = function(){
      mdata.splice(0)
    }

    window.mercuryPush = function(data){
      mdata.push(data)
    }


    var MithrilData = [];
    var mithapp = {
      controller: function() {
      },
      view: function(ctrl) {
        return m("span", [MithrilData.map(function(datum) { return m('span', datum); })]);
      }
    }
    m.module(document.getElementById("mithrilMountNode"), mithapp);

    window.Mclear = function() {
      MithrilData = [];
    };
    window.Mpush = function(data) {
      MithrilData.push(data);
      if (Date.now() > timeout + 16) {
          var id = window.setTimeout(function() {
          m.startComputation();
          m.endComputation();
          window.clearTimeout(id);
        }, 0);

        timeout = Date.now();
      }
    }


    var vueInstance = new Vue({
      el: '#vuejsMountNode',
      data: {
        data: []
      }
    });

    window.VueClear = function(){
      vueInstance.data = []
    };
    window.VuePush = function(data){
      vueInstance.data.push(data);
    };


    Ractive.DEBUG = false;
    var RactiveData = [];
    var RactiveComponent = new Ractive({
      el: '#ractiveMountNode',
      template: '#ractiveTemplate',
      data: { items: RactiveData }
    });

    window.RactiveClear = function() {
      RactiveData.splice(0, RactiveData.length);
    };
    window.RactivePush = function(data) {
      // I don't know how to use plain strings in array in a #each loop
      RactiveData.push({ item: data });
    };


    var riotComponent = riot.mount('riot-component')[0];

    window.RiotClear = function()
    {
      riotComponent.clear();
    }
    window.RiotPush = function(data)
    {
      riotComponent.push(data);
      if (Date.now() > timeout + 16) {
        var id = window.setTimeout(function() {
          riotComponent.update();
          window.clearTimeout(id);
        }, 0);
        timeout = Date.now() + 16;
      }
    }

  }); // document.ready

</script>

    

Preparation code output

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.12/angular.min.js"></script> <script src="http://fb.me/react-0.12.2.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script> <script src="http://builds.emberjs.com/canary/ember-template-compiler.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ember.js/1.11.1/ember.min.js"></script> <script src="https://rawgit.com/Raynos/mercury/master/dist/mercury.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/0.1.34/mithril.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/0.11.5/vue.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ractive/0.7.2/ractive.js"></script> <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/riot/2.0.14/riot.min.js"></script> --> <script src="https://cdn.jsdelivr.net/g/riot@2.0.14(riot.min.js+compiler.min.js)"></script> <!-- Angular --> <div ng-app="ngApp"> Angular (using track by $index): <div ng-controller="Ctrl" id="angList"><span ng-repeat="item in data track by $index">{{item}}</span></div> </div> <!-- Knockout --> <div id="koapp"> Knockout: <div data-bind="foreach: data"><span data-bind="text: $data"></span></div> </div> <!-- Knockout Rate Limit --> <div id="koappRL"> Knockout (rate limit): <div data-bind="foreach: data"><span data-bind="text: $data"></span></div> </div> <!-- Ember --> <div id="emapp"> <script type="text/x-handlebars"> Ember: <div> {{#each EMapp.data}}<span>{{this}}</span>{{/each}} </div> </script> </div> <!-- React --> <div id="react"> React: <div id="reactMountNode"></div> </div> <!-- Mithril --> <div id="mithrilapp"> Mithril: <div id="mithrilMountNode"></div> </div> <!-- Vuejs --> <div id="vuejs"> Vuejs: <div id="vuejsMountNode"><span v-repeat="item: data">{{ item }}</span></div></div> <!-- Mercuryjs --> <div id="mercuryjs"> Mercuryjs: <div id="mercuryjsMountNode"></div> </div> <!-- Ractive --> <script id='ractiveTemplate' type='text/ractive'> {{#each items}}<span>{{item}}</span>{{/each}} </script> <div id="ractive"> Ractive: <div id="ractiveMountNode"></div> </div> <!-- Riot --> <riot-component></riot-component> <script id='riotApp' type='riot/tag'> <riot-component> Riot: <span each={ item, i in items }>{ item }</span> this.items = []; this.clear = function() { this.items.splice(0); // this.update(); } this.push = function(e) { this.items.push(e); // this.update(); } </riot-component> </script> <script> window.getId = function(idx) { var d = Date.now() + ""; return idx + "-" + d.substring(d.length - 6); }; var timeout = 0; var ngApp = angular.module('ngApp',[]); ngApp.controller('Ctrl', function ($scope) { $scope.data = []; }); $(document).ready(function() { angular.element(document).ready(function() { var ang_scope = $('#angList').scope(); window.ANGclear = function(){ ang_scope.data = []; }; window.ANGpush = function(data){ ang_scope.data.push(data); if (Date.now() > timeout + 16) { var id = window.setTimeout(function() { ang_scope.$digest(); window.clearTimeout(id); //console.log("triggered digest"); }, 0); timeout = Date.now() + 16; } }; }); var KOData = ko.observableArray(); var KOviewmodel = {data: KOData}; ko.applyBindings(KOviewmodel, document.getElementById('koapp')); window.KOclear = function (){ KOData.splice(0); }; window.KOpush = function (data){ KOData.push(data); }; var KODataRL = ko.observableArray(); KODataRL.extend({ rateLimit: 0 }); var KOviewmodelRL = {data: KODataRL}; ko.applyBindings(KOviewmodelRL, document.getElementById('koappRL')); window.KOclearRL = function (){ KODataRL.splice(0); }; window.KOpushRL = function (data){ KODataRL.push(data); }; var ENV = {EXTEND_PROTOTYPES: false}; EMapp = Ember.Application.create({ rootElement: '#emapp' }); EMapp.data = Ember.A(); window.EMclear = function () { EMapp.data.clear(); }; window.EMpush = function (data) { EMapp.data.pushObject(data); }; var ReactData = []; var ReactUpdate = null; var ReactComponent = React.createClass({ displayName: 'PerfTest', getInitialState: function() { return { data: ReactData }; }, componentDidMount: function() { ReactUpdate = this._onData; }, render: function() { return React.DOM.span(null, this.state.data.map(function(result) { return React.DOM.span(null, result); }) ); }, _onData: function() { this.setState({ data: ReactData }); } }); var reactComp = ReactComponent(); React.renderComponent(reactComp, document.getElementById('reactMountNode')); window.RClear = function() { ReactData.splice(0); ReactUpdate(); }; window.RPush = function(data) { ReactData.push(data); ReactUpdate(); } var h = mercury.h var mdata = mercury.array([]) mercury.app(document.getElementById('mercuryjsMountNode'), mdata, mrender) function mrender(data) { return h("span", data.map(function(datum) { return h('span', datum)})); } window.mercuryClear = function(){ mdata.splice(0) } window.mercuryPush = function(data){ mdata.push(data) } var MithrilData = []; var mithapp = { controller: function() { }, view: function(ctrl) { return m("span", [MithrilData.map(function(datum) { return m('span', datum); })]); } } m.module(document.getElementById("mithrilMountNode"), mithapp); window.Mclear = function() { MithrilData = []; }; window.Mpush = function(data) { MithrilData.push(data); if (Date.now() > timeout + 16) { var id = window.setTimeout(function() { m.startComputation(); m.endComputation(); window.clearTimeout(id); }, 0); timeout = Date.now(); } } var vueInstance = new Vue({ el: '#vuejsMountNode', data: { data: [] } }); window.VueClear = function(){ vueInstance.data = [] }; window.VuePush = function(data){ vueInstance.data.push(data); }; Ractive.DEBUG = false; var RactiveData = []; var RactiveComponent = new Ractive({ el: '#ractiveMountNode', template: '#ractiveTemplate', data: { items: RactiveData } }); window.RactiveClear = function() { RactiveData.splice(0, RactiveData.length); }; window.RactivePush = function(data) { // I don't know how to use plain strings in array in a #each loop RactiveData.push({ item: data }); }; var riotComponent = riot.mount('riot-component')[0]; window.RiotClear = function() { riotComponent.clear(); } window.RiotPush = function(data) { riotComponent.push(data); if (Date.now() > timeout + 16) { var id = window.setTimeout(function() { riotComponent.update(); window.clearTimeout(id); }, 0); timeout = Date.now() + 16; } } }); // document.ready </script>

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in CCBot 2.0.0 / Other 0.0.0
Test Ops/sec
Angular
ANGclear();
for (var i = 0; i < 100; i++)
{
  ANGpush("nitem" + getId(i));
}
pending…
Knockout
KOclear();
for (var i = 0; i < 100; i++)
{
  KOpush("kitem" + getId(i));
}


pending…
React
RClear();
for (var i = 0; i < 100; i++)
{
  RPush("ritem" + getId(i));
}

pending…
Mithril
Mclear();
for (var i = 0; i < 100; i++)
{
  Mpush("mitem" + getId(i));
}

pending…
vuejs
VueClear();
for (var i = 0; i < 100; i++)
{
  VuePush("vitem" + getId(i));
}
pending…
Ember
EMclear();
for (var i = 0; i < 100; i++)
{
  EMpush("eitem" + getId(i));
}

pending…
Mercury
mercuryClear();
for (var i = 0; i < 100; i++)
{
  mercuryPush("mitem" + getId(i));
}

pending…
Ractive
RactiveClear();
for (var i = 0; i < 100; i++)
{
  RactivePush("aitem" + getId(i));
}

pending…
Knockout Rate Limit
KOclearRL();
for (var i = 0; i < 100; i++)
{
  KOpushRL("kitem" + getId(i));
}

pending…
Riot
RiotClear();
for (var i = 0; i < 100; i++)
{
  RiotPush("ritem" + getId(i));
}
pending…

Compare results of other browsers

Revisions

You can edit these tests or add even more tests to this page by appending /edit to the URL.

1 Comment

PhiLho commented :

Actually this is not a very good test... It is not realistic and quite unfair.

  • Not realistic: a real application will not add data one by one, letting some frameworks to trigger redraw on each update.
  • Unfair: some implementations here use non-standard strategies to coalesce updates to given interval.

We should make a better test:

  • Using a more realistic use case. Eg. adding all data at once (simulating a GET response), sorting it (simulating user clicking on a table header to sort a column) then filtering it (user typing a search query).
  • With two versions: canonical implementation, as shown in the tutorials of the framework; optimized implementation, using the coalescing strategy, if the framework allows it.
Angular (using track by $index):
{{item}}
Knockout:
Knockout (rate limit):
React:
Mithril:
Vuejs:
{{ item }}
Mercuryjs:
Ractive: