The Choking effect of Promises and Async-await on your existing Callbacks
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:
- Simple synchronous callback function
setTimeout
with timeout set to0
as a web API callback function.setTimeout
wrapped inside the Promise.- 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.
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.