I'm adding an answer because a comment would be too long. I originally had a very long, verbose explanation about how async
works and await
work. But it's just so convoluted that actual data may just be easier to understand. So here is the, uh, simplified explanation. Note: This is run on Chrome v97, FireFox v95, and Node v16 with the same results.
The answer as to what's faster: it depends on what you're returning and how you're calling it. await
works differently than async
because it runs PromiseResolve (similar to Promise.resolve
but it's internal). Basically, if you run await
on a Promise (a real one, not a polyfill), await
doesn't try to wrap it. It executes the promise as is. That skips a tick. This is a "newer" change from 2018. In summary, evaluating await
always returns the result of a Promise, not a Promise, while avoiding wrapping Promises when possible. That means await
always takes at least one tick.
But that's await
and async
doesn't actually use this bypass. Now async
uses the good ol' PromiseCapability Record. We care about how this resolves promises. The key points are it'll instantly start fulfilling if the resolution is "not an Object
" or if .then
is not Callable
. If neither are true, (you're returning a Promise
), it'll perform a HostMakeJobCallback
and tack on to the then
in the Promise, which basically means, we're adding a tick. Clarified, if you return a Promise in an async
function, it'll add on an extra tick, but not if you return a Non-Promise.
So, with all that preface (and this is the simplified version), here's your chart as to how many ticks until your await foo()
call is returned:
|
Non-Promise |
Promise |
() => result |
1 |
1 |
async () => result |
1 |
3 |
async () => await result |
2 |
2 |
This is tested with await foo()
. You can also test with foo().then(...)
, but the ticks are the same. (If you don't use an await
, then the sync function would indeed be 0. Though foo().then
would crash, so we need something real to test with.) That means our floor is 1.
If you understood my explanations above (hopefully), this will make sense. The sync function makes sense because at no point in the function do we call for paused execution: await foo()
will take 1 tick.
async
likes Non-Promises and expects them. It will return immediately if it finds one. But if it finds a Promise, it'll tack on to that Promise's then
. That means it'll execute the Promise (+1) and then wait for the then
to complete (another +1). That's why it's 3 ticks.
await
will convert a Promise
to a Non-Promise
which is perfect for async
. If you call await on a Promise
, it'll execute it without tacking any extra ticks (+1). But, await
will convert a Non-Promise
into a Promise
and then run it. That means await
always takes a tick, regardless of what you call it against.
So, in conclusion, if you want the fastest execution, you want to make sure your async
function always includes at least one await
. If it doesn't, then just make it synchronous. You can always call await
on any synchronous function. Now, if you want to really tweak performance, and you are going to use async
, you have to make sure always return a Non-Promise, not a Promise
. If you are returning a Promise
, convert it first with await
. That said you can mix and match like this:
async function getData(id) {
const cache = myCacheMap.get(id);
if (cache) return cache; // NonPromise returns immediately (1 tick)
// return fetch(id); // Bad: Promise returned in async (3 ticks)
return await fetch(id); // Good: Promise to NonPromise via await (2 ticks)
}
With that in mind, I have a bunch of code to rewrite :)
References:
https://v8.dev/blog/fast-async
https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-async-functions-abstract-operations-async-function-start
Test:
async function test(name, fn) {
let tick = 0;
const tock = () => tick++;
Promise.resolve().then(tock).then(tock).then(tock);
const p = await fn();
console.assert(p === 42);
console.log(name, tick);
}
await Promise.all([
test('nonpromise-sync', () => 42),
test('nonpromise-async', async () => 42),
test('nonpromise-async-await', async () => await 42),
test('promise-sync', () => Promise.resolve(42)),
test('promise-async', async () => Promise.resolve(42)),
test('promise-async-await', async () => await Promise.resolve(42)),
]);
setTimeout(() => {}, 100);