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.

Node Underground
Node Underground
Node Underground
How V8 Optimizes async/await: Cutting Microtasks for Faster JavaScript

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Node.jsV8async/awaitmicrotasksJavaScript optimization
Node Underground
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.