JavaScript Timer and Execution Mechanism Analysis

Alexander Parks
6 min readFeb 4, 2024

--

Start with JS Execution Mechanism

The mechanism by which browsers (or JS engines) execute JS is based on an event loop.

Since JS is single-threaded, only one task can be executed at a time, and other tasks have to be queued, and subsequent tasks must wait until the end of the previous task to start execution.

In order to avoid the meaningless wait caused by some long tasks, JS introduces the concept of asynchronous, using another thread to manage asynchronous tasks.

Synchronous tasks execute sequentially directly in the main thread queue, while asynchronous tasks go into another task queue and do not block the main thread. When the main thread queue is empty (the execution is finished), it will go to the asynchronous queue to query whether there are executable asynchronous tasks (asynchronous tasks usually wait for some conditions to be executed after entering the asynchronous queue, such as Ajax requests, file reading and writing). If an asynchronous task can be executed, it will be added to the main thread queue, and so on.

JS Timer

JS currently has three timers: setTimeout, setInterval, and setImmediate.

Timer is also an asynchronous task, usually browsers have a separate timer module, timer delay time by the timer module to manage, when a timer to the executable state, will be added to the main thread queue.

JS timer is very practical, animation must have been used, but also one of the most commonly used asynchronous model.

Sometimes strange problems are solved by adding setTimeout(fn, 0)(hereinafter abbreviated setTimeout(0)). However, if you are not familiar with the timer itself, there will be some strange problems.

setTimeout

setTimeout(fn, x) means fn is executed after a delay of x milliseconds.

Do not trust expectations too much when using it. Strictly speaking, the delay time is always greater than x milliseconds. As for how much, it depends on the implementation of JS at that time.

In addition, if multiple timers are not cleared in time, there will be interference, making the delay time more unpredictable. So, whether or not the timer has run out, it is a good habit to clear the timer that is no longer needed.

HTML5 specifies that the minimum delay time cannot be less than 4ms, i.e. if x is less than 4, it will be treated as 4. However, the implementation of different browsers is different, for example, Chrome can be set to 1ms, IE11/Edge is 4 ms.

setTimeout registered function fn will be handed over to the browser timer module to manage, delay time to add fn to the main process execution queue, if there is still unfinished code in front of the queue, it will take a little time to wait to execute fn, so the actual delay time will be longer than the set. If there is a super-large cycle just before fn, the delay time is not a tiny bit.

(function testSetTimeout() {
const label = 'setTimeout';
console.time(label);
setTimeout(() => {
console.timeEnd(label);
}, 10);
for(let i = 0; i < 100000000; i++) {}
})();

The result is: setTimeout: 335.187ms, much more than 10ms.

setInterval

The setInterval implementation mechanism is similar to setTimeout, except setInterval is repeated.

For setInterval(fn, 100), it is easy to make a mistake: it is not 100ms after the last fn is executed before the next fn is executed. In fact, setInterval does not care about the execution result of the last fn, but puts fn into the main thread queue every 100ms, and the specific interval between two fn is not certain, similar to the actual delay time of setTimeout, and related to JS execution.

(function testSetInterval() {
let i = 0;
const start = Date.now();
const timer = setInterval(() => {
i += 1;
i === 5 && clearInterval(timer);
console.log(`${i}: start`, Date.now() - start);
for(let i = 0; i < 100000000; i++) {}
console.log(`${i}: end`, Date.now() - start);
}, 100);
})();

output:

1: start 100
1: end 1089
2: start 1091
2: end 1396
3: start 1396
3: end 1701
4: start 1701
4: end 2004
5: start 2004
5: end 2307

It can be seen that although the execution time of fn is very long each time, the next time is not to wait for the last execution to be completed and then 100ms to start execution. In fact, it has already been waiting in the queue.

You can also see that when setInterval’s callback takes longer than the delay time, there is no longer any time interval.

If setTimeout and setInterval both execute after a delay of 100ms, then whoever registers first executes the callback function.

setImmediate

This is a relatively new timer, currently IE11/Edge support, Nodejs support, Chrome does not support, other browsers have not been tested.

It’s easy to think of setTimeout(0) from the API name, but setImmediate should be considered an alternative to setTimeout(0).

In IE11/Edge, setImmediate latency can be less than 1ms, while setTimeout has a minimum latency of 4ms, so setImmediate executes the callback function earlier than setTimeout(0). However, in Nodejs, it is possible to execute either first, due to the slight difference between Nodejs event loop and browser.

(function testSetImmediate() {
const label = 'setImmediate';
console.time(label);
setImmediate(() => {
console.timeEnd(label);
});
})();

Edge Output: setImmediate: 0.555 ms

Obviously, setImmediate is designed to ensure that the code is executed on the next event loop, and the previous unreliable setTimeout(0) can be discarded.

Other common asynchronous models

requestAnimationFrame

requestAnimationFrame is not a timer, but it is similar to setTimeout. Browsers without requestAnimationFrame generally use setTimeout to simulate it.

requestAnimationFrame is synchronized with screen refresh. Most screens refresh at 60Hz, and the corresponding requestAnimationFrame triggers about every 16.7 ms. If the screen refresh frequency is higher, requestAnimationFrame will trigger faster. Based on this, it is obviously unwise to animate with setTimeout in browsers that support requestAnimationFrame.

In browsers that do not support requestAnimationFrame, if setTimeout/setInterval is used for animation, the optimal delay time is also 16.7ms. If it is too small, it is likely that the dom will be modified twice or many times in a row before the screen refreshes, so that the frame will be lost and the animation will be stuck; if it is too large, it will obviously have a feeling of stagnation.

Interestingly, the timing of the first trigger of requestAnimationFrame varies from browser to browser, with Edge triggering after about 16.7ms, while Chrome triggers immediately, similar to setImmediate. Arguably, Edge’s implementation seems more logical.

(function testRequestAnimationFrame() {
const label = 'requestAnimationFrame';
console.time(label);
requestAnimationFrame(() => {
console.timeEnd(label);
});
})();

Edge Output: requestAnimationFrame: 16.66 ms

Chrome output: requestAnimationFrame: 0.698ms

However, the time interval between two adjacent requestAnimationFrames is about 16.7ms, which is consistent. Of course, it is not absolute. If the performance of the page itself is relatively low, the time interval may become larger, which means that the page does not reach 60fps.

Promise

Promise is a very common asynchronous model. If we want our code to execute in the next event loop, we can choose setTimeout(0), setImmediate, requestAnimationFrame(Chrome), and Promise.

And Promise has a lower latency than setImmediate, meaning Promise executes before setImmediate.

function testSetImmediate() {
const label = 'setImmediate';
console.time(label);
setImmediate(() => {
console.timeEnd(label);
});
}
function testPromise() {
const label = 'Promise';
console.time(label);
new Promise((resolve, reject) => {
resolve();
}).then(() => {
console.timeEnd(label);
});
}
testSetImmediate();
testPromise();

Edge output:Promise: 0.33 ms, setImmediate: 1.66 ms

Although setImmediate’s callback function is registered before Promise, Promise executes first.

What is certain is that in each JS environment, Promise is executed first, setTimeout(0), setImmediate, and requestAnimationFrame are not in the same order.

process.nextTick

process.nextTick is an API for Nodejs that executes earlier than Promise.

In fact, process.nextTick does not enter the asynchronous queue, but directly inserts a task at the end of the main thread queue. Although it does not block the main thread, it will block the execution of asynchronous tasks. If there is a nested process.nextTick, the asynchronous task will never be executed.

Be careful when you use it, unless your code explicitly wants to execute before the end of this event loop, otherwise it is safer to use setImmediate or Promise.

--

--

No responses yet