Author: SSH

The cause of

As most people know, setTimeout has a minimum delay time. According to the MDN document setTimeout: The actual delay is longer than the set value.

In browsers, setTimeout()/setInterval() has a minimum interval of 4ms per call, usually due to function nesting (nesting level up to a certain level).

The HTML Standard specification is also more specific:

Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.

In simple terms, nesting of timers above 5 levels results in a delay of at least 4ms.

Do a test with the following code:

let a = performance.now();
setTimeout(() = > {
  let b = performance.now();
  console.log(b - a);
  setTimeout(() = > {
    let c = performance.now();
    console.log(c - b);
    setTimeout(() = > {
      let d = performance.now();
      console.log(d - c);
      setTimeout(() = > {
        let e = performance.now();
        console.log(e - d);
        setTimeout(() = > {
          let f = performance.now();
          console.log(f - e);
          setTimeout(() = > {
            let g = performance.now();
            console.log(g - f);
          }, 0);
        }, 0);
      }, 0);
    }, 0);
  }, 0);
}, 0);
Copy the code

The print in the browser looks something like this, in line with the specification, with a delay of more than 4ms on the fifth execution.

For more detailed reasons, see why setTimeout has a minimum delay of 4ms.

explore

What if we just needed an “execute now” timer? Is there any way around the 4ms delay? There are some clues in the corner of the MDN document above:

If you want to implement the 0ms timer in your browser, you can refer to window.postmessage () here.

The author of this article gives a code that uses postMessage to implement a true 0 delay timer:

(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.

Use the nested timer example above to run the test:

All are in the range of 0.1 to 0.3 ms, and the latency does not increase as the number of nesting layers increases.

test

In theory, since the postMessage implementation is not limited by the browser engine, it must be faster than setTimeout. But there’s no proof. Let’s use data.

The author designed an experimental method, which is to use postMessage timer and traditional timer to perform a recursive counting function operation respectively, to see how much time it takes to count to 100. Readers can also run the quiz for themselves here.

Experiment code:

function runtest() {
  var output = document.getElementById('output');
  var outputText = document.createTextNode(' ');
  output.appendChild(outputText);
  function printOutput(line) {
    outputText.data += line + '\n';
  }

  var i = 0;
  var startTime = Date.now();
  // Reach the 100 count by recursive setZeroTimeout
  // When 100 is reached, switch to setTimeout to experiment
  function test1() {
    if (++i == 100) {
      var endTime = Date.now();
      printOutput(
        '100 iterations of setZeroTimeout took ' +
          (endTime - startTime) +
          ' milliseconds.'
      );
      i = 0;
      startTime = Date.now();
      setTimeout(test2, 0);
    } else {
      setZeroTimeout(test1);
    }
  }

  setZeroTimeout(test1);

  // Reach the 100 count by recursive setTimeout
  function test2() {
    if (++i == 100) {
      var endTime = Date.now();
      printOutput(
        '100 iterations of setTimeout(0) took ' +
          (endTime - startTime) +
          ' milliseconds.'
      );
    } else {
      setTimeout(test2, 0); }}}Copy the code

The experiment code is very simple, first counting to 100 recursively with setZeroTimeout, the postMessage version, and then switching to setTimeout to count to 100.

In conclusion, the gap is not fixed, but after using traceless mode on my MAC to eliminate plug-ins and other factors, it is about 80 to 100 times the time difference when I count to 100, for example. On my desktop computer, which has better hardware, it’s 200 times more.

The Performance panel

Not enough to just look at cold numbers, let’s open up the Performance panel to see how the postMessage and setTimeout timers are distributed in a more intuitive visualization.

This distribution diagram shows all of the phenomena we mentioned above intuitively. The postMessage version on the left is very densely distributed, completing all counting tasks within about 5ms.

Compared with the setTimeout version on the right, the distribution is sparse, and it can be seen from the time axis above that the interval of the first four executions is about 1ms, and the interval of the fifth execution is more than 4ms.

role

Some students may ask, what scenario requires a delayless timer? The React source code uses the time slice section.

React Scheduler uses MessageChannel to implement a piece of pseudocode from this article:

const channel = new MessageChannel();
const port = channel.port2;

// Each port.postmessage () call adds a macro task
// The macro task is to call the scheduler.scheduletask method
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
  scheduleTask() {
    // Pick a task and execute it
    const task = pickTask();
    const continuousTask = task();

    // If the current task is not completed, the next macro task continues
    if (continuousTask) {
      port.postMessage(null); }}};Copy the code

React splits tasks into many pieces, allowing the browser main thread to take back control of more priority rendering tasks (such as user input) by handing them over to postMessage’s callback function.

Why not perform microtasks that are more timely? The key to understanding EventLoop’s relationship to browser rendering, frame animation, and idle callbacks is that the microtask is executed before rendering, so that even if the browser has an urgent render task, it has to wait for the microtask to finish rendering.

conclusion

Through this article, you can probably understand the following points:

  1. setTimeout4ms delay historical reasons, specific performance.
  2. How to usepostMessageImplement a true 0 delay timer.
  3. postMessageThe application of timer in React time slice.
  4. Why time slice needs macro tasks instead of micro tasks.