Here's the jsperf: http://jsperf.com/promise-vs-callback
callback case (211 Ops/s):
// async test
var d = deferred;
function getData(callback) {
setTimeout(function() {
callback('data')
}, 0)
}
getData(function(data) {
d.resolve()
})
Promise case(614 ops/s):
// async test
var d = deferred;
function getData() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve('data')
}, 0);
})
}
getData().then(function(data) {
d.resolve()
})
As you see promise are way faster, but they have more code. The question is why this happens.
Here deferred
is to defined by jsperf to show it as the completion of the async test.
As it seems the magic trick lies with in how chrome sets the minimum delay for setTimeout(fn, 0)
.
I searched for it and I found this: https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/Hn3GxRLXmR0/XP9xcY_gBPQJ
I quote the important part:
The way timer clamping works is every task has an associated timer nesting level. If the task originates from a setTimeout() or setInterval() call, the nesting level is one greater than the nesting level of the task that invoked setTimeout() or the task of the most recent iteration of that setInterval(), otherwise it's zero. The 4ms clamp only applies once the nesting level is 4 or higher. Timers set within the context of an event handler, animation callback, or a timer that isn't deeply nested are not subject to the clamping.
In the callback case, setTimeout is called recursively, in a context of another setTimeout , so the minimum timeout is 4ms.
In the promise case, setTimeout is actually not called recursively, so the minimum timeout is 0(It wouldn't be actually 0, because other stuff has to run too).
So how do we know setTimeout is called recursively? well we can just conduct an experiment in jsperf or just using benchmark.js:
// async test
deferred.resolve()
Which will result in Uncaught RangeError: Maximum call stack size exceeded.
Which means, once deferred.resolve is called, the test is run again on the same tick/stack. So in the callback case setTimeout is called in it's own calling context and nested in another setTimeout, which will set the minimum timeout to 4ms.
But in the promise case, .then
callback is called after the next tick according to promise spec, and v8 doesn't use setTimeout calling the callback after the next tick. It uses something that must be similar to process.nextTick in nodejs or setImmediate, and not setTimeout. Which resets the setTimeout nesting level to 0 again and makes the setTimeout delay 0ms.
First of all, your benchmark is designed wrong. It is only going to measure the minimal setTimeout
value, not the perf difference between callbacks and promises.
The minimal delay is 4ms
so the result cannot be more than 250 operations per second. Somehow calling new Promise
is removing the minimal 4ms delay.
If you wanted to measure promise and callback difference, you need to remove such unnatural bottlenecks. So not only are you measuring at concurrency level of 1, you are waiting 4ms between each call.
JSPErf doesn't make it easy to set concurrency, but here is with concurrency = 1000:
http://jsperf.com/promise-vs-callback/7
As Esailija pointed it's related to weird setTimeout optimizaition in Promise.
See also same benchmark made with faster setTimeout alternative: http://jsperf.com/promise-vs-callback/8 it gives more expected results