We have extended our previous benchmarks to include other popular DOM manipulation libraries: Angular 1 and 2, Mithril.js, cito.js and the standalone independent implementation of React's Virtual DOM algorithm. We have also added more metrics (including memory use). Read on! Update: we have released an updated version of this article including the use of track-by and array key ids, check it out.
Introduction
In case you haven't read our previous benchmarks article, here is what we did: we took a sample implementation of DBMonster along with the excellent benchmark scripts and tools from this post, reimplemented it for use with different DOM libraries, ran the scripts and collected the results. For this post we have also extended browser-perf to generate memory usage statistics.
We will submit our additions to browser-perf for review, which is why we haven't linked to the original browser-perf repo above.
What follows is an overview of the contenders. Or you can jump straight to the results.
Angular.js 1.x
The venerable Angular 1.x library is part of an opinionated but powerful framework that saw its initial release in the year 2009. Since then it has gathered a tremendous following and inspired many other libraries. Often criticized for being hard to integrate into existing projects, and questioned for its performance in certain use cases, it is still used in many high-ranking websites. Angular extends standard HTML with custom tags and a template syntax (which form the view). Controllers are defined following a series of strict API requirements that include the use of dependency injection. Data binding and model changes are handled through a digest cycle. The digest cycle is the method Angular uses to detect changes in the model that trigger changes in the view. This method has particular performance characteristics as can be seen in the results below.
An Angular 1.x view template:
<td ng-repeat="query in database.topFiveQueries"
ng-class="getClassName(query)">
{{formatElapsed(query.elapsed)}}
<div class="popover left">
<div class="popover-content">
{{query.query}}
</div>
<div class="arrow"></div>
</div>
</td>
And part of the associated JavaScript code, including dependency injection semantics:
dbmonControllers.controller('MainController', ['$scope', '$timeout',
function($scope, $timeout) {
$scope.loadCount = 0;
$scope.databases = {};
$scope.getClassName = function(query) {
var className = "elapsed short";
if (query.elapsed >= 10.0) {
className = "elapsed warn_long";
} else if (query.elapsed >= 1.0) {
className = "elapsed warn";
}
return "Query " + className;
};
//(...)
}
);
Angular.js 2
The still-in-flux Angular 2 library has recently been declared in beta state. Still under heavy scrutiny by many developers that do not consider a breaking change to be the right way to fix all of Angular 1.x problems, Angular 2 carries on with its opinionated approach. TypeScript is now favored (but not required) over JavaScript and a new template syntax is now required. The digest cycle was replaced by the use of a change detection algorithm that walks the DOM tree. Other optimizations allow Angular 2 to detect precisely when the model has changed (in contrast to the need to explicitly tell Angular 1 so in certain cases).
An Angular 2 view template:
<td *ngFor="#query of database.topFiveQueries"
[ngClass]="getClassName(query)">
{{formatElapsed(query.elapsed)}}
<div class="popover left">
<div class="popover-content">
{{query.query}}
</div>
<div class="arrow"></div>
</div>
</td>
And its associated JavaScript code:
app.AppComponent = ng.core
.Component({
selector: 'my-app',
templateUrl: './app/component.html'
//directives: [angular.NgFor]
})
.Class({
//(...)
getClassName: function(query) {
var className = "elapsed short";
if (query.elapsed >= 10.0) {
className = "elapsed warn_long";
} else if (query.elapsed >= 1.0) {
className = "elapsed warn";
}
return "Query " + className;
}
]);
Our Angular 2 DBMonster code was developed using JavaScript rather than the recommended TypeScript to reuse as much code as possible from other versions of the benchmark. Angular 2 code is more idiomatic using TypeScript and was developed with its features in mind.
Virtual DOM
Virtual DOM is an independent implementation of React's tree-diffing algorithm. It provides an API that allows users to describe a DOM tree directly in JavaScript. JSX is not used.
function renderQuery(query) {
var className = "elapsed short";
if (query.elapsed >= 10.0) {
className = "elapsed warn_long";
} else if (query.elapsed >= 1.0) {
className = "elapsed warn";
}
return h('td', { className: 'Query ' + className }, [
query.elapsed ? formatElapsed(query.elapsed) : '',
h('div', { className: 'popover left' }, [
h('div', { className: 'popover-content' }, query.query),
h('div', { className: 'arrow' })
])
]);
}
Mithril.js
A React-like library aimed at being simple, small and fast. It provides a convenient API to describe DOM-trees in JavaScript, and a preprocessor to turn DOM-tree API-calls into simple JSON objects for additional speed. The API for DOM-trees is similar to that of Virtual DOM, and the required controller/view objects are simple enough to be integrated easily into existing bodies of code. Much like Angular 1, it requires changes to the DOM tree made outside library boundaries to be notified for redrawing. Data-binding is performed through a properties system.
function renderQuery(query) {
var className = "elapsed short";
if (query.elapsed >= 10.0) {
className = "elapsed warn_long";
} else if (query.elapsed >= 1.0) {
className = "elapsed warn";
}
return m('td', { className: 'Query ' + className }, [
query.elapsed ? formatElapsed(query.elapsed) : '',
m('div', { className: 'popover left' }, [
m('div', { className: 'popover-content' }, query.query),
m('div', { className: 'arrow' })
])
]);
}
cito.js
A minimalist Virtual DOM-like library. A virtual DOM is constructed from plain JSON-like objects. A simple call tells the library to compare the existing tree to a new one and perform the necessary updates.
function renderQuery(query) {
var className = "elapsed short";
if (query.elapsed >= 10.0) {
className = "elapsed warn_long";
} else if (query.elapsed >= 1.0) {
className = "elapsed warn";
}
return {
tag: 'td',
attrs: { 'class': 'Query ' + className },
children: [
query.elapsed ? formatElapsed(query.elapsed) : '',
{
tag: 'div',
attrs: { 'class': 'popover left' },
children: [
{
tag: 'div',
attrs: { 'class': 'popover-content' },
children: query.query.toString()
}, {
tag: 'div',
attrs: { 'class': 'arrow' }
}
]
}
]
};
}
Ember 1
Another powerful and opinionated framework. Ember makes use of their own template language which allows for easy differentiation between static and dynamic parts of the DOM. Their diffing algorithm is optimized to take this into account. Data binding is handled through a properties system.
An Ember view using its template syntax:
{{#each topFiveQueries key="key" as |query|}}
<td class="Query {{query.className}}">
{{query.elapsed}}
<div class="popover left">
<div class="popover-content">{{query.query}}</div>
<div class="arrow"></div>
</div>
</td>
{{/each}}
And its associated JavaScript code:
export default Ember.Component.extend({
tagName: 'tr',
queries: function() {
var samples = this.get('attrs.db.value.samples');
return samples[samples.length - 1].queries;
}.property('attrs.db'),
topFiveQueries: function() {
var queries = this.get('queries');
var topFiveQueries = queries.slice(0, 5);
while (topFiveQueries.length < 5) {
topFiveQueries.push({ query: "" });
}
return topFiveQueries.map(function(query, index) {
return {
key: index+'',
query: query.query,
elapsed: query.elapsed ? formatElapsed(query.elapsed) : '',
className: elapsedClass(query.elapsed)
};
});
}.property('queries'),
//(...)
});
Ember 2
This newer version of Ember removes many deprecated parts of the library and serves as a cleanup of the API. Most of the other benefits of Ember 2 can also be found in Ember 1.13+ releases.
The Ember 2 code in this article is 100% compatible with the Ember 1.x version, so no example is provided here.
React.js
Facebook's popular library is gaining ground day-by-day. Its simple integration model, flexibility and speed make it a no-brainer for many projects. The crux of React is its smart diffing algorithm: a virtual tree is constructed by making JavaScript calls. When a new DOM tree is constructed, React can find the optimal number of operations to transform the old tree into the new one. React is usually paired with the JSX preprocessor, which allows an extended form of HTML to be embedded in JavaScript to describe components in a convenient way.
var Query = React.createClass({
render: function() {
var className = "elapsed short";
if (this.props.elapsed >= 10.0) {
className = "elapsed warn_long";
}
else if (this.props.elapsed >= 1.0) {
className = "elapsed warn";
}
return (
<td className={"Query " + className}>
{this.props.elapsed ? formatElapsed(this.props.elapsed) : ''}
<div className="popover left">
<div className="popover-content">{this.props.query}</div>
<div className="arrow"/>
</div>
</td>
);
}
})
Incremental DOM
A Google project, Incremental DOM aims to develop a memory-efficient library that can perform in-place updates of the DOM tree. It is intended to be used as a compilation target for different template languages. In practice, its API is similar to that of Virtual DOM, Mithril or cito.js. The main benefit of a memory efficient approach is reduced waits during garbage collection cycles. This can result in improved performance and fewer dropped frames during rendering.
function renderQuery(query) {
var className = "elapsed short";
if (query.elapsed >= 10.0) {
className = "elapsed warn_long";
} else if (query.elapsed >= 1.0) {
className = "elapsed warn";
}
elementOpen('td', null, null, 'class', "Query " + className);
text(query.elapsed ? formatElapsed(query.elapsed) : '');
elementOpen('div', null, ['class', 'popover left']);
elementOpen('div', null, ['class', 'popover-content']);
text(query.query);
elementClose('div');
elementVoid('div', null, ['class', 'arrow']);
elementClose('div');
elementClose('td');
}
The Results
Here are the summarized results of our tests:
Check out the full code for all tests.
Aside: React at Auth0
At Auth0 we care about maintainability and performance. We routinely look at competing solutions to find out the right tool for the job. For our Passwordless Lock library, we found that React has great performance characteristics, allows for an easy integration path, and results in clean, readable code. At the same time, it sports an active community that allows our developers to quickly and easily find how to get the job done. If you are interested in seeing how we use React, check out the source!
Conclusion
We are consistently surprised by the results of the Incremental DOM library. Not only in numbers, but also in the way it feels faster than the alternatives. Google has done a tremendous job optimizing its internals to produce a faster and leaner library. The only other library that has achieved this subjective feeling of speed is cito.js. In the case of cito.js we are concerned by its apparent lack of development activity at GitHub. When it comes to bigger frameworks, we cannot recommend React enough. It sports the right balance of performance, memory use, support and community mindshare. Mithril.js surprised us with its ease of use and great performance. We consider it to be a rightful contender to React, with an all-around great balance between speed, memory usage and ease of use. Between the bigger frameworks, both Angular and Ember are good choices. We consider them great for many use cases: in particular, for developers that adhere to their specific ideologies (if you like TypeScript, you will love Angular 2 as a library that is developed for it from the ground-up). Lastly, Angular 1.x performance issues are by now known by all developers. If you want to use it, take this into consideration. In particular, consider that Angular 2 is significantly better than Angular 1 in every metric (to be fair, Angular 1 and 2 are different enough to be considered totally different libraries, only inspired by similar concepts).
"Mithril.js surprised us with its ease of use and great performance."
Tweet This
Some oddities in the results (for instance cito.js appearing slower in dropped frame counts and FPS counts when subjective tests tell otherwise) make us think something else is going on behind the scenes, so take these results as what they are: a small and probably biased sample. Our methodology and testing tools probably need an improvement.
Let us know in the comments what you think! Cheers!