3 minute read

Your Promise and Async-await may inadvertently be choking your Callbacks and adversely impacting the user experience.

Introduction

You want to make long calculations in your JavaScript program and decide to use promise/async-await to carry out the heavy lifting hoping to render your webpage at the earliest for the consistent user experience and relegate the heavy lift calculations to be carried out behind the scenes.

However, after incorporating promise/async-await in your program, you may find that some of the webpage components, which were wrapped inside the callbacks are not getting rendered as expected and the performance of your page has actually been hit.

Hhmm!!! that calls for some introspection. We need to be mindful of the impact of promise/async-await functions on our existing callbacks and the resulting cascading effect on the user experience. Let us try to understand as to what is happening with the help small code snippet. The code below performs following functions:

  1. Simple synchronous callback function
  2. setTimeout with timeout set to 0 as a web API callback function.
  3. setTimeout wrapped inside the Promise.
  4. Async-await doing the heavy lift calculations.
Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
  import { hrtime } from 'node:process';
  const startTime = hrtime.bigint();

  function banner(name) {
    const diffTime = hrtime.bigint() - startTime;
    console.log(`${diffTime} : ${name}`);
  }

  banner('Started Main');

  // Section 1: Simple synchronous callback function
  const syncFuncCallback = (name)=>name('Inside synchronous function callback');
  syncFuncCallback(banner);

  // Section 2: `setTimeout` with timeout set to `0` as a callback function.
  setTimeout(()=>{
    banner('Inside async callback');
  }, 0);

  // Section 3: `setTimeout` wrapped inside the Promise.
  const _ = new Promise(resolve => {
      setTimeout(() => {
        banner('Inside wrapped callback');
        resolve();
      }, 0);
    });

  // Section 4: Async-await doing the heavy lift calculations.
  async function longCalculation() {
    const _ = await new Promise(resolve => resolve());
    banner('Inside async microtask: awaiting for long calculation to finish');
    for (let i = 0; i < 5; i++) {
      for (let j = 0; j < 1000000000; j++);
      banner('Waiting...');
    }
  };

  // Async-await function being called multiple times
  longCalculation();
  longCalculation();

  banner('Finished Main');
  
Output
  
  36900 : Started Main
  3578600 : Inside synchronous function callback
  4752400 : Finished Main
  5192000 : Inside async microtask: awaiting for long calculation to finish
  817667900 : Waiting...
  1632834400 : Waiting...
  2042245200 : Waiting...
  2448202900 : Waiting...
  2853205800 : Waiting...
  2853816200 : Inside async microtask: awaiting for long calculation to finish
  3664469800 : Waiting...
  4070420800 : Waiting...
  4474460300 : Waiting...
  4877473000 : Waiting...
  5283404000 : Waiting...
  5285441000 : Inside async callback
  5286196200 : Inside wrapped callback
  


In the output we observe that the setTimeOut callback is executing at the end of the program even though it has the timeout interval of 0 milliseconds, whereas the async-await function having much longer execution time due to heavy lift calculation is being given priority and executed before the callback. It is basically choking our callbacks. So, what’s happening here!!! This unexpected behavior is the one that may adversely impact the rendering or responsiveness of our webpage resulting in poor user experience.

Explanation

In order to understand this behavior we need to look how V8 JavaScript Engine handles callbacks, promises and async-await under the hood. The diagram here shows the JavaScript runtime.

Deployment Architecture of Application

The promises are relatively new to JavaScript. Earlier we had only one callback queue or task queue to handle asynchronous callbacks which were part of Web API. With the introduction of promises/async-await JavaScript had a native way to handle asynchronous code and the JavaScript committee decided to have an additional queue to handle these promises/async-await through what is called job queue or microtask queue.

The microtask queue is similar to the task queue or the callback queue but has a higher priority than the callback queue. This means that the event loop is going to check the microtask queue first and make sure that there’s nothing in that queue before it starts looking at the callback queue or the task queue. Thus we see, why promise/async-await always executes before our Web API callbacks.