<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro">
<link rel="stylesheet" href="circles.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/lodash/lodash/3.0.1/lodash.min.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.0/react-with-addons.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.0/react-dom.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<script src="https://unpkg.com/can@3.2.1/dist/global/can.all.js"></script>
</head>
<body>
<div class="intro">
<p>Run the test below to see how DoneJS' view engine beats the competition by performing minimal DOM updates when properties are changed.</p>
<p>The test will run 1000 updates in each framework and record how long each update takes. Each update will animate the circles, change their text and background color.</p>
</div>
<button class="animate" onclick="runTest()">Run Test</button>
<p id="timing"></p>
<div style="display: none;" id="testResults">
<p>The most performant approach is to touch the DOM as little as possible.Angular performs the worst because there are a large number of objects to watch while React is closer with its diffing approach.</p>
<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
</div>
<div id="grid"></div>
<div ng-app="animationApp">
<div ng-controller="animationCtrl">
<div class="grid">
<div class="box-view" ng-repeat="box in boxes">
<div class="box" ng-style="{top: box.top+'px', left: box.left+'px', background: 'rgb(0,0,'+box.color+')'}">{{box.content}}</div>
</div>
</div>
</div>
</div>
<script type="text/stache" id="stache-template">
{{#each boxes}}
<div class="box-view">
<div class="box" id="{{number}}" style="{{style}}">
{{content}}
</div>
</div>
{{/each}}
</script>
</body>
</html>
body {
font-family: "Lato", sans-serif;
background: #FCFEFD;
padding: 5px;
}
p {
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #424a4e;
}
.animate, .reset {
border: none;
cursor: pointer;
width: 100%;
color: #fff;
height: 35px;
padding: 5px;
display: block;
margin: 0 auto;
line-height: 1.5em;
background: #3B83A3;
font-weight: 700;
font-size: 14px;
}
.reset {
background: #393E40 ;
}
button {
font: bold 14px/14px Lato;
margin-left: 10px;
}
#grid, .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;
}
/* globals _ */
// noprotect
var N = 100; //Number of circles
var L = 1000; //Number of loops to run for each library
//CanJS
(function() {
var boxes = new can.DefineList();
var Box = can.DefineMap.extend({
count: { value: 0 },
top: { value: 0 },
left: { value: 0 },
color: { value: 0 },
content: { value: 0 },
style: { value: '' },
tick: function() {
var count = this.count + 1;
this.count = count;
this.top = Math.sin(count / 10) * 10;
this.left = Math.cos(count / 10) * 10;
this.color = count % 255;
this.content = count % 100;
this.style = this.computeStyle();
},
computeStyle: function() {
return 'top: ' + this.top + 'px; left: ' + this.left + 'px; background: rgb(0,0,' + this.color + ');';
}
});
for (var i = 0; i < 100; i++) {
var box = new Box({ number: i });
box.tick();
boxes.push(box);
}
var canAnimate = function() {
can.batch.start();
for (var i = 0; i < boxes.length; i++) {
boxes[i].tick();
}
can.batch.stop();
};
window.runCan = function() {
window.reset();
window.currentBenchmark = 'can';
var template = document.getElementById('stache-template').innerHTML;
$('#grid').append(can.stache(template)({
boxes: boxes
}));
window.benchmarkLoop(canAnimate);
};
})();
//React
/* globals React, ReactDOM */
(function() {
var counter;
var BoxView = React.createClass({
render: function() {
var count = this.props.count + 1;
return React.DOM.div({ className: "box-view" }, React.DOM.div({
className: "box",
style: {
top: Math.sin(count / 10) * 10,
left: Math.cos(count / 10) * 10,
background: 'rgb(0, 0,' + count % 255 + ')'
}
}, count % 100));
}
});
var BoxesView = React.createClass({
render: function() {
var boxes = _.map(_.range(N), function(i) {
return React.createFactory(BoxView)({ key: i, count: this.props.count });
}, this);
return React.DOM.div(null, boxes);
}
});
var reactInit = function() {
counter = -1;
reactAnimate();
};
var reactAnimate = function() {
ReactDOM.render(
React.createFactory(BoxesView)({ count: counter++ }),
document.getElementById('grid')
);
};
window.runReact = function() {
window.reset();
window.currentBenchmark = 'react';
reactInit();
window.benchmarkLoop(reactAnimate);
};
})();
//Angular
/* global angular */
(function() {
var animationApp = angular.module('animationApp', []);
animationApp.controller('animationCtrl', function($scope, $rootScope, $window) {
$scope.count = 0;
$scope.boxes = new Array();
$scope.Box = function(n) {
this.number = n;
this.top = 0;
this.left = 0;
this.color = 0;
this.content = 0;
this.count = 0;
this.tick = function() {
var count = this.count += 1;
this.top = Math.sin(count / 10) * 10;
this.left = Math.cos(count / 10) * 10;
this.color = count % 255;
this.content = count % 100;
};
};
$scope.angularjsInit = function() {
for (var i = 0; i < window.N; i++) {
$scope.boxes[i] = new $scope.Box(i);
}
};
$scope.angularjsAnimate = function() {
for (var i = 0; i < window.N; i++) {
$scope.boxes[i].tick();
}
$rootScope.$apply();
};
$window.angularReset = function() {
$scope.boxes = new Array();
$rootScope.$apply();
};
$window.runAngularjs = function() {
$scope.boxes = new Array();
$window.reset();
$window.currentBenchmark = 'angular';
$scope.angularjsInit();
window.benchmarkLoop($scope.angularjsAnimate);
};
});
})();
window.runTest = function() {
$('#testResults').hide();
$('#timing').text('').show();
window.runCan();
};
window.reset = function() {
$('#grid').empty();
window.angularReset();
clearTimeout(window.timeout);
window.updateResults();
window.currentBenchmark = null;
window.totalTime = null;
window.loopCount = null;
};
window.timeResults = {
can: null,
react: null,
angular: null
};
window.updateResults = function() {
if (window.currentBenchmark !== null) {
window.timeResults[window.currentBenchmark] = (window.totalTime / window.loopCount).toFixed(2);
}
};
window.timeout = null;
window.totalTime = null;
window.loopCount = null;
window.benchmarkLoop = function(fn) {
var startDate = new Date();
fn();
var endDate = new Date();
window.totalTime += endDate - startDate;
window.loopCount++;
if (window.loopCount % 20 === 0) {
$('#timing').text(
window.currentBenchmark.charAt(0).toUpperCase() + window.currentBenchmark.slice(1) +
': Performed ' + window.loopCount + ' updates in ' + window.totalTime + ' ms (average ' +
(window.totalTime / window.loopCount).toFixed(2) + ' ms per update).'
);
}
if (window.loopCount < L) {
window.timeout = _.defer(window.benchmarkLoop, fn);
}
else {
if (window.currentBenchmark === 'can') {
window.runReact();
}
else if (window.currentBenchmark === 'react') {
window.runAngularjs();
}
else {
window.reset();
window.renderGraph();
}
}
};
window.renderGraph = function() {
console.log([ +window.timeResults.can, +window.timeResults.react, +window.timeResults.angular ]);
$('#testResults').show();
$('#timing').hide();
$('#container').highcharts({
chart: {
type: 'bar'
},
plotOptions: {
bar: {
dataLabels: {
enabled: true
}
}
},
colors: [ '#3B83A3' ],
tooltip: {
valueSuffix: 'ms'
},
title: {
text: null
},
xAxis: {
categories: [ 'CanJS', 'React', 'Angular' ],
title: {
text: null
}
},
yAxis: {
title: {
text: 'Time per update (ms)'
},
plotLines: [ {
value: 0,
width: 1,
color: '#808080'
} ]
},
legend: {
enabled: false
},
series: [ {
data: [ +window.timeResults.can, +window.timeResults.react, +window.timeResults.angular ]
} ]
});
};
Output
You can jump to the latest bin by adding /latest
to your URL
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |