How V8 Optimizes async/await: Cutting Microtasks for Faster JavaScript
This article explains how V8 reduces the number of microtasks created by async/await by eliminating redundant Promise wrappers and streamlining then‑handlers, showing the original transformation code, the optimizations applied, and the resulting execution order in both browsers and Node.js.
In the previous article we mentioned that V8's async optimization saves an extra Promise and two Microtasks; this article dives into exactly which two Microtasks are optimized.
Consider the simple example:
async function foo() {
const w = await v;
return w;
}The diagram from the earlier post illustrated how V8 handles the await. The equivalent JavaScript transformation is:
function foo(v) {
const implicit_promise = new Promise((resolve, reject) => {
const promise = Promise.resolve().then(() => v);
const throwaway = new Promise((resolve, reject) => {
promise.then(resolve, reject);
}).then(result => {
resumeFoo(result);
}).catch(err => {
throwFoo(err);
});
const resumeFoo = function (result) {
const returnValue = (v => {
// original foo logic
const w = v;
return w;
})(result);
resolve(returnValue);
};
const throwFoo = err => {
reject(err);
};
});
return implicit_promise;
}When JavaScript runs, each then is queued as a Microtask, which are processed after each event‑loop turn. From the code above we can see the three Microtasks:
First Microtask :
const promise = Promise.resolve().then(() => v);Regardless of whether v is already a Promise, Promise.resolve().then wraps it, creating an unnecessary Promise. V8 detects this case and skips the extra wrapper, saving a Microtask.
Second Microtask :
promise.then(resolve, reject);Third Microtask :
const throwaway = new Promise((resolve, reject) => {
promise.then(resolve, reject);
}).then(result => {
resumeFoo(result);
}).catch(err => {
throwFoo(err);
});The throwaway block can be simplified to:
promise.then(resumeFoo, throwFoo);This removes another Microtask, yielding the final optimized version:
function foo() {
const implicit_promise = new Promise((resolve, reject) => {
v.then(resumeFoo, throwFoo);
const resumeFoo = function (result) {
const returnValue = (v => {
// original foo logic
const w = v;
return w;
})(result);
resolve(returnValue);
};
const throwFoo = err => {
reject(err);
};
});
return implicit_promise;
}Running the following code demonstrates the effect:
const p = Promise.resolve();
(async () => {
await p; console.log('after:await');
})();
p.then(() => console.log('tick:a'))
.then(() => console.log('tick:b'));Transformed for Node 12, the execution logic becomes:
const p = Promise.resolve();
(async () => {
const implicit_promise = new Promise((resolve, reject) => {
v.then(resumeFoo, throwFoo);
const resumeFoo = function (result) {
const returnValue = (v => {
console.log('after:await');
})(result);
resolve(returnValue);
};
const throwFoo = err => {
reject(err);
};
});
return implicit_promise;
})();
p.then(() => console.log('tick:a'))
.then(() => console.log('tick:b'));The execution order is:
First then fires, queuing resumeFoo as a Microtask.
Second then fires, queuing console.log('tick:a') as a Microtask.
Microtasks run, outputting after:await then tick:a.
Third then queues console.log('tick:b'), which runs after the previous Microtasks, outputting tick:b.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Node Underground
No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
