background

SetTimeout is a macro task that is counted by the browser’s timer thread with a minimum interval of 4ms. It will be blocked by eventLoop, resulting in an unexpected execution time. SetTimeout can simulate setInterval, if used to drive animation, when tags are minimized or hidden, Animation speeds up when activated again.

This was all I knew about setTimeout until a big screen display project with second refresh. After the release, the user raised a problem. When the page was minimized, there was a problem with data synchronization.

At that time, my first reaction was that this was not a code BUG at all, because it was a very unreasonable operation to display a large screen being minimized. However, I calmed down and wrote a test code, and the test result completely overturned my understanding of setTimeout.

Therefore, I plan to take this opportunity to learn and understand setTimeout thoroughly. Next, let’s uncover the unknown veil of setTimeout layer by layer.

Note: There are many reasons affecting the setTimeout delay. The increase of setTimeout delay caused by a single Event loop delay is not considered here, which is simplified to the most original case.

The return value of setTimeout

Anyone wondering what the number printed in the Chrome console when you type in setTimeout is?

In fact, this is the return value of setTimeout, is a non-repeating number, you can use this ID to cancel the execution of setTimeout.

The returned timeoutID is a positive integer value which identifies the timer created by the call to setTimeout(). This value can be passed to clearTimeout() to cancel the timeout.

It is guaranteed that a timeoutID value will never be reused by a subsequent call to setTimeout() or setInterval() on the same object (a window or a worker). However, different objects use separate pools of IDs.

From the description of MDN, we can know that setTimeout() and setInterval() share the same number pool, and clearTimeout() and clearInterval() are technically interchangeable. However, to avoid confusion, do not mix the cancel timing functions.

Also, setTimeout() or setInterval() will not reuse the same timer number in subsequent calls to the same object (a window or worker). But different objects use separate number pools.

const id = setTimeout(() = > {
  console.log('Will not be executed')},1000)

clearInterval(id) // It is possible to cancel the setTimeout execution above, but not recommended

Copy the code

SetTimeout Maximum delay time

The minimum delay time of setTimeout is 4ms, but I don’t know whether you know that setTimeout has a maximum delay time.

Let’s look at the description of the maximum value of setTimeout in MDN:

Maximum delay value

Browsers including Internet Explorer, Chrome, Safari, and Firefox store the delay as a 32-bit signed integer internally. This causes an integer overflow when using delays larger than 2,147,483,647 ms (about 24.8 days), resulting in the timeout being executed immediately.

Browsers including Internet Explorer, Chrome, Safari, and Firefox have an internal memory delay as a 32-bit signed integer. This causes a delay greater than 2,147,483,647 milliseconds (approximately 24.8 days) to overflow, causing the timer to be executed immediately.


setTimeout(() = > {

  console.log('Right now! ')},2147483647 + 1)

Copy the code

This code is executed immediately because the maximum latency is exceeded.

Note that setTimeout() and setInterval() share the same number pool, and clearTimeout() and clearInterval() are technically interchangeable. However, to avoid confusion, do not mix the cancel timing functions.

On the same object (a window or worker), setTimeout() or setInterval() will not reuse the same timer number in subsequent calls. But different objects use separate number pools.

SetTimeout Minimum delay time

This is probably one of the most familiar questions you’ll ever ask in a job interview, but do you really know anything about minimum latency?

Is the minimum latency really 4ms?

Let’s do an experiment. Here’s the code

const a = performance.now()
let i = 1

setTimeout(() = > {
  const b = performance.now()
  console.log('number of cycles${i}: `, b - a)
  i++
  setTimeout(() = > {
    const c = performance.now()
    console.log('number of cycles${i}: `,c - b)
    i++
    setTimeout(() = > {
      const d = performance.now()
      console.log('number of cycles${i}: `, d - c)
      i++
      setTimeout(() = > {
        const e = performance.now()
        console.log('number of cycles${i}: `, e - d)
        i++
        setTimeout(() = > {
          const f = performance.now()
          console.log('number of cycles${i}: `, f - e)
          i++
          setTimeout(() = > {
            const g = performance.now()
            console.log('number of cycles${i}: `, g - f)
            i++
          }, 0)},0)},0)},0)},0)},0)
Copy the code

Output result:

We can see the execution result, the delay time of the first four times is less than 4ms, but from the fifth time, the delay time increases significantly (>4ms), it seems that the minimum delay time of 4ms cannot fully explain the results of this experiment, why is this?

Source of the minimum delay time of 4ms

As the saying goes, you never know what’s going on.

We can see the description of minimum latency in the HTML Standard, updated February 11, 2022:

After the timer is nested for 5 layers, the time interval is forced to be set to a minimum of 4ms;

If timeout is less than 0, set it to 0.

If the nesting level is greater than 5 and timeout is less than 4, set to 4.

It can be seen that the document clearly states that after nesting 5 layers, the time interval is forced to be 4ms, which also explains why the above experiment results are less than 4ms for the first 4 times, and more than 4ms after the fourth time. The legend about 4ms should also come from this.

However, the documentation makes it clear that the 4ms minimum is mandatory only when timers are nested and exceed 5 layers, which is not always followed by browser vendors. In this case, 4ms is mandatory at layer 5.

Differences: Standard >5 layers of nesting, Chrome >=5 layers of nesting.

But there was a new question.

What is the minimum delay for a timer when it is not nested or nested with less than 5 layers?

To answer this question, let’s look at a test case:

const fn = () = > {
  setTimeout(() = > {
    console.log('first')},1)
  setTimeout(() = > {
    console.log('second')},0)
}

fn()
Copy the code

Who will print it first? Think about 5s.

Output result:

Why is a 1ms delay executed before a 0ms delay? What if we reverse the order?

const fn = () = > {
  setTimeout(() = > {
    console.log('second')},0)

  setTimeout(() = > {
    console.log('first')},1)
}

fn()
Copy the code

Output result:

The result is changed, it seems that the output order of 0ms and 1ms delay time is related to the execution order, now try to analyze, 0ms and 1ms delay time is less than or equal to the minimum execution time, so the final output order is consistent with the execution order.

At this point, let’s try to guess what is the minimum execution time? Given that 4ms is only valid for nesting and greater than 5 layers, and initial tests have shown that the minimum latency can be less than 4ms(j is close to 1ms), it’s a bold guess that Chrome’s minimum latency is 1ms (for Chrome only), and then do a test to find out.


const fn = () = > {
  setTimeout(() = > {
    console.log('second')},2)
  
  setTimeout(() = > {
    console.log('first')},1)
}

fn()
Copy the code

Output result:

Why does setTimeout have a minimum delay of 4ms? The boss listed the Chromium source code in the article to support, the answer is already clear.


static const int maxIntervalForUserGestureForwarding = 1000; // One second matches Gecko.

static const int maxTimerNestingLevel = 5;

static const double oneMillisecond = 0.001;

// Chromium uses a minimum timer interval of 4ms. We'd like to go

// lower; however, there are poorly coded websites out there which do

// create CPU-spinning loops. Using 4ms prevents the CPU from

// spinning too busily and provides a balance between CPU spinning and

// the smallest possible interval timer.

static const double minimumInterval = 0.004;

double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond);

if (intervalMilliseconds < minimumInterval && m_nestingLevel >= maxTimerNestingLevel)

intervalMilliseconds = minimumInterval;

Copy the code

The code logic is clear, setting three constants:

MaxTimerNestingLevel = 5. This is the nesting level referred to in HTML Standard

MinimumInterval = 0.004. This is what THE HTML Standard calls minimum latency.

As we will see in the second code, we first take a maximum between the delay time and 1ms. In other words, the minimum delay time is set to 1ms in cases where the nesting hierarchy is not satisfied. This also explains why testing 0ms and 1ms in Chrome is the output above.

SetTimeout has a minimum delay of 4ms. This article not only elaborates the minimum value of setTimeout in detail, but also describes the history of delay time update and the game and tradeoff strategy between browser manufacturers and system platforms and HTML specifications, which is worth a look.

Implement setTimeout with 0 delay

Now that you know the minimum latency, have you ever wondered how to implement a true zero-latency setTimeout?

The setTimeout documentation also provides an answer:

Use the window.postmessage () method to do this.

(function () {
  var timeouts = [];
  var messageName = 'zero-timeout-message';
  // Keep setTimeout, only accept arguments of a single function, and the delay is always 0.
  function setZeroTimeout(fn) {
    timeouts.push(fn);
    window.postMessage(messageName, The '*');
  }
  function handleMessage(event) {
    if (event.source == window && event.data == messageName) {
      event.stopPropagation();
      if (timeouts.length > 0) {
        varfn = timeouts.shift(); fn(); }}}window.addEventListener('message', handleMessage, true);
  // Add API to window object
  window.setZeroTimeout = setZeroTimeout; }) ();Copy the code

Since the postMessage callback is a macro task similar to setTimeout, you can simply use the message notification combination of postMessage and addEventListener(‘message’) to simulate the timer function.

This completes a timer with similar execution timing but with a smaller delay.

Let’s test the nested results of the first use case with this modified zero-delay timer.

const a = performance.now()
let i = 1
window.setZeroTimeout(() = > {
  const b = performance.now()
  console.log('number of cycles${i}: `, b - a)
  i++
  window.setZeroTimeout(() = > {
    const c = performance.now()
    console.log('number of cycles${i}: `,c - b)
    i++
    window.setZeroTimeout(() = > {
      const d = performance.now()
      console.log('number of cycles${i}: `, d - c)
      i++
      window.setZeroTimeout(() = > {
        const e = performance.now()
        console.log('number of cycles${i}: `, e - d)
        i++
        window.setZeroTimeout(() = > {
          const f = performance.now()
          console.log('number of cycles${i}: `, f - e)
          i++
          window.setZeroTimeout(() = > {
            const g = performance.now()
            console.log('number of cycles${i}: `, g - f)
            i++
          }, 0)},0)},0)},0)},0)},0)
Copy the code

Output result:

You can see that the results are all <1ms and the latency does not increase as the number of nesting layers increases.

It seems that we have implemented a timer that is much more real-time than setTimeout. The advantage of this timer is that there are a lot of call timers (especially nested calls) that are frequent and spaced close to the minimum delay time. A timer with a zero delay would be very efficient, almost a hundred times more efficient than setTimeout.

SetTimeout delay mechanism in inactive tabs

Now, let’s go back to the user’s question at the beginning of this article, why are setTimeout driven data requests having problems when pages are minimized?

Before this, I knew something about setTimeout being slow in inactive tabs, but never specifically.

After reviewing the document, I found the following description:

As described in the documentation, the minimum delay is forced to be >=1000ms (1s) in the inactive tabs.

However, our second large screen update time is also <= 1000ms(because setTimeout will be blocked by eventLoop in actual application, each update delay is 1s- block time, otherwise the block time will accumulate). If this is the case, there will not be a wide lag in data synchronization.

Let’s test it out.

let lastTime = new Date().getTime()
const fn = () = > {
  const currentTime = new Date().getTime()
  const diffTime = currentTime - lastTime
  lastTime = currentTime
  setTimeout(fn, 500)
  console.log('Time interval:'.`${diffTime}ms`)}setTimeout(fn, 500)
Copy the code

We designed a timing function to execute once for 500ms, then minimized the page and left it inactive, forcing the delay to 1s after some time.

Output result:

As you can see, it’s pretty much in line with the documentation, but let’s minimize the page a little longer.

Output result:

We can see that the delay goes from 1s to 1min in vain. Why is that?

SetTimeout Time budget

I found the answer in the page Visibility API on MDN.

We can see a key word repeatedly in this document, which is summarized as follows:

To address the impact of timers in hidden tabs on performance, browsers adopt the following policies when tags are not activated:

  1. The callback function defined in requestAnimationFrame() will not be called to improve battery life;

  2. The setTimeout delay is longer than the specified delay.

  3. Unactivated tabs each window has its own time budget, Firefox (in ms), Chrome (in S);

  4. Firefox Windows are restricted 30 seconds later, while Chrome is restricted 10 seconds later.

  5. When the timer completes, the elapsed time is subtracted from the time budget of the window in which it is executed.

  6. Timer tasks are allowed only when time budget >=0;

  7. In Firefox and Chrome, the budget is regenerated at a rate of 10ms/s until the time budget >=0, and the timer is executed again.

What is the time budget in inactive state?

This is not found in the document, if any friends have relevant documents, please let me know.

However, we can still test the scope of this budget.

Here’s a screenshot of the result when the test code above forces a 1s delay:

Executed 300 times, so Chrome’s time budget is about 300 seconds, or 5 minutes.

How about solving the problem of inconsistent delay times in inactive tabs?

The solution has been provided in MDN documentation.

The summary is as follows:

  1. The TAB that is playing audio is considered active, so it is not affected;

  2. Tabs that use WebSocket and WebRTC are not affected;

  3. The IndexedDB process is also unaffected.

Therefore, to sum up, the best solution to achieve a second large screen should be WebSocket push mode, rather than setTimeout pull mode, of course, this needs background support.

However, if a project like ours can only use setTimeout to retrieve data, the best solution would be:

  1. Call the VisiBilityChange event when the page is inactive to stop the setTimeout data request and record the timestamp of the last request + 1s;

  2. When the page is activated, the visiBilityChange event is called again, and all the data recorded when the trigger is not activated to the current time is directly obtained through another interface;

  3. After the data is returned, restart the timer for second level requests.

Reference documentation

  • The HTML standard

  • MDN-setTimeout

  • MDN- Page invisible API

  • Why does setTimeout have a minimum delay of 4ms?